import { ref, nextTick, onBeforeUnmount, Ref } from 'vue' export function useThumbnails(getFilePath: (p: string) => string) { const thumbnailMap = ref>({}) const generatingMap = new Map() const placeholderDataUrl = 'data:image/svg+xml;utf8,' function isGenerating(key: string) { return generatingMap.has(key) } function startGenerating(key: string) { generatingMap.set(key, new AbortController()) } function stopGenerating(key: string) { generatingMap.get(key)?.abort() generatingMap.delete(key) } async function generateThumbnail(imagePath: string, actionId: string, maxW = 400, maxH = 400) { if (!imagePath) return const key = `${imagePath}::${actionId}` if (thumbnailMap.value[imagePath] !== undefined) return if (isGenerating(key)) return startGenerating(key) try { const src = getFilePath(imagePath) const img = new Image() img.src = src await new Promise((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] = null 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] = null 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] = null } finally { stopGenerating(key) } } let observer: IntersectionObserver | null = null function observe(containerRef: Ref, goodsListRef: Ref) { 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 const actionId = img?.action_id || img?.action_name || '' if (p && thumbnailMap.value[p] === undefined) { generateThumbnail(p, actionId, 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 } generatingMap.forEach((ctrl) => ctrl.abort()) generatingMap.clear() } onBeforeUnmount(() => { stop() }) return { thumbnailMap, generateThumbnail, observe, stop, placeholderDataUrl, } }