|
|
@@ -0,0 +1,109 @@
|
|
|
+import { ref, nextTick, onBeforeUnmount, Ref } from 'vue'
|
|
|
+
|
|
|
+export function useThumbnails(getFilePath: (p: string) => string) {
|
|
|
+ const thumbnailMap = ref<Record<string, string>>({})
|
|
|
+ const generatingSet = new Set<string>()
|
|
|
+ const placeholderDataUrl =
|
|
|
+ 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="300" height="200"><rect width="100%" height="100%" fill="#F5F7FA"/></svg>'
|
|
|
+
|
|
|
+ async function generateThumbnail(imagePath: string, maxW = 400, maxH = 400) {
|
|
|
+ if (!imagePath) return
|
|
|
+ if (thumbnailMap.value[imagePath]) return
|
|
|
+ if (generatingSet.has(imagePath)) return
|
|
|
+ generatingSet.add(imagePath)
|
|
|
+ try {
|
|
|
+ const src = getFilePath(imagePath)
|
|
|
+ const img = new Image()
|
|
|
+ img.src = src
|
|
|
+ await new Promise<void>((resolve, reject) => {
|
|
|
+ img.onload = () => resolve()
|
|
|
+ img.onerror = () => reject(new Error('image load error'))
|
|
|
+ })
|
|
|
+ const w = img.naturalWidth || img.width
|
|
|
+ const h = img.naturalHeight || img.height
|
|
|
+ if (!w || !h) {
|
|
|
+ thumbnailMap.value[imagePath] = ''
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const ratio = Math.min(1, maxW / w, maxH / h)
|
|
|
+ const tw = Math.max(1, Math.round(w * ratio))
|
|
|
+ const th = Math.max(1, Math.round(h * ratio))
|
|
|
+ const canvas = document.createElement('canvas')
|
|
|
+ canvas.width = tw
|
|
|
+ canvas.height = th
|
|
|
+ const ctx = canvas.getContext('2d')
|
|
|
+ if (!ctx) {
|
|
|
+ thumbnailMap.value[imagePath] = ''
|
|
|
+ return
|
|
|
+ }
|
|
|
+ ctx.drawImage(img, 0, 0, tw, th)
|
|
|
+ const dataUrl = canvas.toDataURL('image/jpeg', 0.7)
|
|
|
+ thumbnailMap.value[imagePath] = dataUrl
|
|
|
+ } catch (e) {
|
|
|
+ console.warn('generateThumbnail error', e)
|
|
|
+ thumbnailMap.value[imagePath] = ''
|
|
|
+ } finally {
|
|
|
+ generatingSet.delete(imagePath)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ let observer: IntersectionObserver | null = null
|
|
|
+
|
|
|
+ function observe(containerRef: Ref<HTMLElement | null>, goodsListRef: Ref<any[]>) {
|
|
|
+ if (!containerRef.value) return
|
|
|
+ if (observer) observer.disconnect()
|
|
|
+ observer = new IntersectionObserver(
|
|
|
+ (entries) => {
|
|
|
+ entries.forEach((entry) => {
|
|
|
+ if (entry.isIntersecting) {
|
|
|
+ const el = entry.target as HTMLElement
|
|
|
+ const goodsNo = el.getAttribute('data-goods-art-no')
|
|
|
+ if (!goodsNo) return
|
|
|
+ const item = goodsListRef.value.find((g: any) => g.goods_art_no === goodsNo)
|
|
|
+ if (item && Array.isArray(item.items)) {
|
|
|
+ item.items.forEach((img: any) => {
|
|
|
+ const p = img?.PhotoRecord?.image_path
|
|
|
+ if (p && !thumbnailMap.value[p]) generateThumbnail(p, 400, 400)
|
|
|
+ })
|
|
|
+ }
|
|
|
+ observer?.unobserve(el)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ },
|
|
|
+ { root: null, rootMargin: '300px', threshold: 0.05 },
|
|
|
+ )
|
|
|
+
|
|
|
+ nextTick(() => {
|
|
|
+ const nodes = containerRef.value!.querySelectorAll('.history-item, .goods-item')
|
|
|
+ nodes.forEach((n) => {
|
|
|
+ if (n instanceof HTMLElement) {
|
|
|
+ const keyEl = n.querySelector('.goods-art-no') as HTMLElement | null
|
|
|
+ const key = keyEl ? keyEl.innerText : n.getAttribute('data-goods-art-no') || ''
|
|
|
+ if (key) n.setAttribute('data-goods-art-no', key)
|
|
|
+ observer?.observe(n)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ function stop() {
|
|
|
+ if (observer) {
|
|
|
+ observer.disconnect()
|
|
|
+ observer = null
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ onBeforeUnmount(() => {
|
|
|
+ stop()
|
|
|
+ })
|
|
|
+
|
|
|
+ return {
|
|
|
+ thumbnailMap,
|
|
|
+ generateThumbnail,
|
|
|
+ observe,
|
|
|
+ stop,
|
|
|
+ placeholderDataUrl,
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+
|