useThumbnails.ts 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. import { ref, nextTick, onBeforeUnmount, Ref } from 'vue'
  2. export function useThumbnails(getFilePath: (p: string) => string) {
  3. const thumbnailMap = ref<Record<string, string | null>>({})
  4. const generatingMap = new Map<string, AbortController>()
  5. const placeholderDataUrl =
  6. '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>'
  7. function isGenerating(key: string) {
  8. return generatingMap.has(key)
  9. }
  10. function startGenerating(key: string) {
  11. generatingMap.set(key, new AbortController())
  12. }
  13. function stopGenerating(key: string) {
  14. generatingMap.get(key)?.abort()
  15. generatingMap.delete(key)
  16. }
  17. async function generateThumbnail(imagePath: string, actionId: string, maxW = 400, maxH = 400) {
  18. if (!imagePath) return
  19. const key = `${imagePath}::${actionId}`
  20. if (thumbnailMap.value[imagePath] !== undefined) return
  21. if (isGenerating(key)) return
  22. startGenerating(key)
  23. try {
  24. const src = getFilePath(imagePath)
  25. const img = new Image()
  26. img.src = src
  27. await new Promise<void>((resolve, reject) => {
  28. img.onload = () => resolve()
  29. img.onerror = () => reject(new Error('image load error'))
  30. })
  31. const w = img.naturalWidth || img.width
  32. const h = img.naturalHeight || img.height
  33. if (!w || !h) {
  34. thumbnailMap.value[imagePath] = null
  35. return
  36. }
  37. const ratio = Math.min(1, maxW / w, maxH / h)
  38. const tw = Math.max(1, Math.round(w * ratio))
  39. const th = Math.max(1, Math.round(h * ratio))
  40. const canvas = document.createElement('canvas')
  41. canvas.width = tw
  42. canvas.height = th
  43. const ctx = canvas.getContext('2d')
  44. if (!ctx) {
  45. thumbnailMap.value[imagePath] = null
  46. return
  47. }
  48. ctx.drawImage(img, 0, 0, tw, th)
  49. const dataUrl = canvas.toDataURL('image/jpeg', 0.7)
  50. thumbnailMap.value[imagePath] = dataUrl
  51. } catch (e) {
  52. console.warn('generateThumbnail error', e)
  53. thumbnailMap.value[imagePath] = null
  54. } finally {
  55. stopGenerating(key)
  56. }
  57. }
  58. let observer: IntersectionObserver | null = null
  59. function observe(containerRef: Ref<HTMLElement | null>, goodsListRef: Ref<any[]>) {
  60. if (!containerRef.value) return
  61. if (observer) observer.disconnect()
  62. observer = new IntersectionObserver(
  63. (entries) => {
  64. entries.forEach((entry) => {
  65. if (entry.isIntersecting) {
  66. const el = entry.target as HTMLElement
  67. const goodsNo = el.getAttribute('data-goods-art-no')
  68. if (!goodsNo) return
  69. const item = goodsListRef.value.find((g: any) => g.goods_art_no === goodsNo)
  70. if (item && Array.isArray(item.items)) {
  71. item.items.forEach((img: any) => {
  72. const p = img?.PhotoRecord?.image_path
  73. const actionId = img?.action_id || img?.action_name || ''
  74. if (p && thumbnailMap.value[p] === undefined) {
  75. generateThumbnail(p, actionId, 400, 400)
  76. }
  77. })
  78. }
  79. observer?.unobserve(el)
  80. }
  81. })
  82. },
  83. { root: null, rootMargin: '300px', threshold: 0.05 },
  84. )
  85. nextTick(() => {
  86. const nodes = containerRef.value!.querySelectorAll('.history-item, .goods-item')
  87. nodes.forEach((n) => {
  88. if (n instanceof HTMLElement) {
  89. const keyEl = n.querySelector('.goods-art-no') as HTMLElement | null
  90. const key = keyEl ? keyEl.innerText : n.getAttribute('data-goods-art-no') || ''
  91. if (key) n.setAttribute('data-goods-art-no', key)
  92. observer?.observe(n)
  93. }
  94. })
  95. })
  96. }
  97. function stop() {
  98. if (observer) {
  99. observer.disconnect()
  100. observer = null
  101. }
  102. generatingMap.forEach((ctrl) => ctrl.abort())
  103. generatingMap.clear()
  104. }
  105. onBeforeUnmount(() => {
  106. stop()
  107. })
  108. return {
  109. thumbnailMap,
  110. generateThumbnail,
  111. observe,
  112. stop,
  113. placeholderDataUrl,
  114. }
  115. }