UNPKG

fs-virtual-waterfall

Version:

Vue3 虚拟瀑布流组件

278 lines (277 loc) 9.25 kB
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 };