fs-virtual-waterfall
Version:
Vue3 虚拟瀑布流组件
278 lines (277 loc) • 9.25 kB
JavaScript
import { defineComponent as P, useCssVars as X, ref as Z, reactive as W, computed as u, watch as T, nextTick as I, markRaw as E, onMounted as ee, onUnmounted as te, openBlock as v, createElementBlock as g, normalizeClass as q, createElementVNode as M, normalizeStyle as C, Fragment as L, renderList as z, renderSlot as V, createCommentVNode as x, toDisplayString as R, pushScopeId as le, popScopeId as ne } from "vue";
function ae(s) {
let c = !1;
return function(...e) {
c || (c = !0, window.requestAnimationFrame(() => {
s.apply(this, e), c = !1;
}));
};
}
function oe(s, c = 300) {
let e = null;
return function(...i) {
e && clearTimeout(e), e = setTimeout(() => {
s.apply(this, i);
}, c);
};
}
const A = (s) => (le("data-v-78a52c03"), s = s(), ne(), s), ie = {
key: 1,
class: "fs-virtual-waterfall-loading"
}, se = /* @__PURE__ */ A(() => /* @__PURE__ */ M("div", { class: "loading-spinner" }, null, -1)), re = /* @__PURE__ */ A(() => /* @__PURE__ */ M("div", { class: "loading-text" }, "加载中...", -1)), ue = [
se,
re
], ce = {
key: 2,
class: "fs-virtual-waterfall-empty"
}, de = /* @__PURE__ */ P({
__name: "FsVirtualWaterFall",
props: {
data: {},
loading: { type: Boolean, default: !1 },
hasMore: { type: Boolean, default: !0 },
gap: { default: 20 },
column: { default: 4 },
columnItemCount: { default: 6 },
themeMode: { default: "auto" },
cardStyle: { default: "rounded" },
scrollBehavior: { default: "smooth" },
emptyText: { default: "暂无数据" },
initialHeight: { default: 1e3 },
loadMoreThreshold: { default: 200 },
hoverScale: { default: 1.02 },
debug: { type: Boolean, default: !1 }
},
emits: ["loadMore", "loadError"],
setup(s, { emit: c }) {
const e = s;
X((t) => ({
"5b2b462a": e.hoverScale
}));
const i = Z(), o = W({
list: [],
error: null
}), m = W({
viewWidth: 0,
viewHeight: 0,
start: 0,
get end() {
return this.start + this.viewHeight;
}
}), r = W({
queue: Array(e.column).fill(0).map(() => ({ list: [], height: 0 })),
len: 0
}), N = u(() => !e.loading && o.list.length === 0), Q = u(() => ({
[`theme-${e.themeMode}`]: !0,
"debug-mode": e.debug
})), D = u(() => ({
[`card-${e.cardStyle}`]: !0
})), f = u(() => {
if (!m.viewWidth)
return 0;
const t = Math.max(e.column, 1), n = (t - 1) * e.gap, l = m.viewWidth - n;
return Math.floor(l / t);
}), G = () => {
r.queue = Array(e.column).fill(0).map(() => ({ list: [], height: 0 })), r.len = 0;
};
T(
() => e.column,
() => {
G(), o.list.length > 0 && I(() => {
H(), _(e.column * e.columnItemCount);
});
},
{ immediate: !1 }
), T(
() => e.data,
(t) => {
t && t.length > 0 && (o.list = E([...t]), I(() => {
H(), _(o.list.length);
}));
},
{ immediate: !0, deep: !0 }
), T(
() => f.value,
() => {
o.list.length > 0 && f.value > 0 && I(() => {
H();
});
}
);
const O = u(() => ({
// 完全匹配内容高度,不添加额外留白
height: `${w.value.maxHeight}px`,
scrollBehavior: e.scrollBehavior
})), U = u(() => ({
position: "absolute",
left: "0",
width: "100%",
// 直接放在内容底部,不添加额外空间
bottom: "0",
transform: "translateY(0)",
padding: "10px 0",
zIndex: 10
})), k = u(() => r.queue.reduce((t, { list: n }) => t.concat(n), [])), $ = u(() => !k.value || k.value.length === 0 ? [] : k.value.filter((t) => t.h + t.y > m.start - 500 && t.y < m.end + 500)), w = u(() => {
let t = 0, n = 0, l = 1 / 0, a = -1 / 0;
return r.queue.forEach(({ height: d }, h) => {
d < l && (l = d, t = h), d > a && (a = d, n = h);
}), a <= 0 && (a = e.initialHeight), {
minIndex: t,
maxIndex: n,
minHeight: l,
maxHeight: a
};
}), Y = u(
() => o.list.reduce((t, n) => {
if (f.value <= 0)
return t;
const l = n.width / Math.max(n.height, 1), a = Math.floor(f.value / l);
return console.log(f.value, "xxx"), console.log(l, "aspectRatio"), t.set(n.id, {
width: f.value,
height: a
}), t;
}, /* @__PURE__ */ new Map())
), S = u(() => r.len < o.list.length), _ = (t = e.column) => {
for (let n = 0; n < t; n++) {
if (!S.value)
return;
const l = w.value.minIndex, a = r.queue[l];
if (!a)
return;
const d = a.list[a.list.length - 1] || null, h = o.list[r.len];
if (!h)
return;
const p = j(h, d, l);
if (!p)
return;
a.list.push(p);
const y = a.list.length > 1 ? e.gap : 0;
a.height += p.h + y, r.len++;
}
}, j = (t, n, l) => {
if (!t)
return null;
const a = Y.value.get(t.id);
if (!a)
return null;
const d = a.width || 0, h = a.height || 0;
let p = 0;
n && (p = n.y + n.h + e.gap);
const y = 200;
return console.log(h, y), console.log(h + y, "height + contentBuffer"), E({
item: t,
y: p,
h: h + y,
// 为底部添加适当空间
style: {
width: `${d}px`,
height: `${h + y}px`,
// 同步增加样式高度
// 使用绝对定位的left/top替代transform,解决定位问题
left: `${l * (d + e.gap)}px`,
top: `${p}px`,
"--index": l
// 为动画设置索引
}
});
}, H = () => {
r.queue = Array(e.column).fill(0).map(() => ({ list: [], height: 0 })), r.len = 0, o.list.length > 0 && _(o.list.length);
}, b = ae(() => {
if (!i.value)
return;
const { scrollTop: t, clientHeight: n } = i.value;
m.start = t;
const l = w.value.maxHeight - w.value.minHeight;
if (!e.loading && e.hasMore) {
const a = t + n > w.value.minHeight - e.loadMoreThreshold;
!S.value && a || l < n * 0.5 && !S.value ? c("loadMore", r.len) : S.value && a && _(e.column * 2);
}
}), B = oe(() => {
F(), H();
}, 300), F = () => {
if (!i.value)
return;
const { clientWidth: t, clientHeight: n, scrollTop: l } = i.value;
m.viewWidth = t || 0, m.viewHeight = n || 0, m.start = l || 0;
}, J = () => {
if (F(), o.list.length > 0) {
const t = Math.min(o.list.length, Math.max(e.column * e.columnItemCount * 3, o.list.length));
_(t), setTimeout(() => {
if (i.value) {
const { scrollHeight: n, clientHeight: l } = i.value;
n <= l && e.hasMore && c("loadMore", r.len);
}
}, 100);
}
i.value && i.value.addEventListener("scroll", b), window.addEventListener("resize", B);
}, K = () => {
i.value && i.value.removeEventListener("scroll", b), window.removeEventListener("resize", B);
};
return ee(() => {
I(() => {
J();
});
}), te(() => {
K();
}), (t, n) => (v(), g("div", {
class: q(["fs-virtual-waterfall-container", Q.value])
}, [
M("div", {
class: "fs-virtual-waterfall-content",
ref_key: "contentRef",
ref: i
}, [
M("div", {
class: "fs-virtual-waterfall-list",
style: C(O.value)
}, [
$.value && $.value.length > 0 ? (v(!0), g(L, { key: 0 }, z($.value, ({ item: l, style: a }, d) => (v(), g("div", {
class: "fs-virtual-waterfall-item",
key: l.id,
style: C(a)
}, [
M("div", {
class: q(["fs-virtual-waterfall-item-inner", D.value])
}, [
V(t.$slots, "item", { item: l }, void 0, !0)
], 2)
], 4))), 128)) : x("", !0),
t.loading && o.list.length === 0 ? (v(), g("div", ie, ue)) : x("", !0),
N.value ? (v(), g("div", ce, R(e.emptyText || "暂无数据"), 1)) : x("", !0),
o.list.length > 0 ? (v(), g("div", {
key: 3,
class: "fs-virtual-waterfall-footer",
style: C(U.value)
}, [
V(t.$slots, "footer", {}, void 0, !0)
], 4)) : x("", !0),
e.debug && f.value > 0 ? (v(!0), g(L, { key: 4 }, z(e.column, (l) => (v(), g("div", {
key: `col-${l}`,
class: "fs-virtual-waterfall-debug-column",
style: C({
left: `${(l - 1) * (f.value + e.gap)}px`,
width: `${f.value}px`,
height: `${Math.max(...r.queue.map((a) => a.height), 500)}px`
})
}, " Column " + R(l), 5))), 128)) : x("", !0)
], 4)
], 512)
], 2));
}
});
const he = (s, c) => {
const e = s.__vccOpts || s;
for (const [i, o] of c)
e[i] = o;
return e;
}, fe = /* @__PURE__ */ he(de, [["__scopeId", "data-v-78a52c03"]]), ve = {
install: (s) => {
s.component("FsVirtualWaterFall", fe);
}
};
export {
fe as FsVirtualWaterFall,
oe as debounce,
ve as default,
ae as rafThrottle
};