diff --git a/resources/assets/js/functions/web.js b/resources/assets/js/functions/web.js index 336164bee..39ddbc6b2 100755 --- a/resources/assets/js/functions/web.js +++ b/resources/assets/js/functions/web.js @@ -1,11 +1,15 @@ import {MarkdownPreview} from "../utils/markdown"; import {convertLocalResourcePath} from "../components/Replace/utils"; +import {imageFailedSrc, isImageFailed} from "../utils/imageFailed"; /** * 页面专用 */ (function (window) { const $ = window.$A; + + // 已加载失败的图片直接输出占位图,避免重新渲染时反复请求 + const imageSrcAttr = src => isImageFailed(src) ? `src="${imageFailedSrc}" data-failed-src="${src}"` : `src="${src}"`; /** * ============================================================================= * ******************************* web extra ******************************* @@ -405,10 +409,12 @@ import {convertLocalResourcePath} from "../components/Replace/utils"; const value = item.original .replace(/\s+width=/, ` original-width=`) .replace(/\s+height=/, ` original-height=`) - .replace(/\s+src=(["'])([^'"]*)\1/i, ` style="width:${data.width}px;height:${data.height}px" src="${convertLocalResourcePath(data.src)}"`) + .replace(/\s+src=(["'])([^'"]*)\1/i, ` style="width:${data.width}px;height:${data.height}px" ${imageSrcAttr(convertLocalResourcePath(data.src))}`) text = text.replace(item.original, value) } else { - text = text.replace(item.original, `
${item.original}
`); + const value = item.original + .replace(/\s+src=(["'])([^'"]*)\1/i, (match, quote, src) => ` ${imageSrcAttr(src)}`) + text = text.replace(item.original, `
${value}
`); } }) return text; diff --git a/resources/assets/js/pages/manage/components/DialogWrapper.vue b/resources/assets/js/pages/manage/components/DialogWrapper.vue index c9202ce0e..bee63376b 100644 --- a/resources/assets/js/pages/manage/components/DialogWrapper.vue +++ b/resources/assets/js/pages/manage/components/DialogWrapper.vue @@ -181,7 +181,8 @@
+ v-longpress="{callback: handleLongpress, preventEndEvent: true}" + @error.capture="onImageError">
@@ -758,6 +759,7 @@ import Forwarder from "./Forwarder/index.vue"; import {throttle} from "lodash"; import transformEmojiToHtml from "../../../utils/emoji"; import {webhookEventOptions} from "../../../utils/webhook"; +import {imageFailedSrc, markImageFailed, unmarkImageFailed} from "../../../utils/imageFailed"; export default { name: "DialogWrapper", @@ -2515,6 +2517,20 @@ export default { } }, + // 图片加载失败后换成占位图并登记,消息重新渲染时不再请求(Safari 不缓存失败图片,会反复请求) + onImageError({target}) { + if (target.tagName !== 'IMG' || target.dataset.failedSrc !== undefined) { + return + } + const src = target.getAttribute('src') + if (!src || src.startsWith('data:')) { + return + } + markImageFailed(src) + target.dataset.failedSrc = src + target.src = imageFailedSrc + }, + onPointerover({pointerType}) { this.pointerMouse = pointerType === 'mouse'; }, @@ -3936,6 +3952,13 @@ export default { switch (target.nodeName) { // 打开图片 case "IMG": + // 加载失败的图片点击重试 + if (target.dataset.failedSrc !== undefined) { + unmarkImageFailed(target.dataset.failedSrc) + target.src = target.dataset.failedSrc + delete target.dataset.failedSrc + break + } if (!(target.classList.contains('browse') && this.onViewPicture(target.currentSrc))) { const list = $A.getTextImagesInfo(el.outerHTML) this.$store.dispatch("previewImage", {index: target.currentSrc, list}) diff --git a/resources/assets/js/utils/imageFailed.js b/resources/assets/js/utils/imageFailed.js new file mode 100644 index 000000000..e495e9399 --- /dev/null +++ b/resources/assets/js/utils/imageFailed.js @@ -0,0 +1,28 @@ +/** + * 消息内加载失败的图片登记 + * + * Safari 不缓存失败(如 404)的图片,虚拟列表回收/重挂载消息时 v-html 会重建 , + * 导致反复请求;且失败图片尺寸塌陷又会触发列表重新测量,形成循环。 + * 这里按地址记住失败的图片,重新渲染时直接输出占位图,不再请求。 + */ + +// 透明 1x1 gif,失败样式由 img[data-failed-src] 提供 +export const imageFailedSrc = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" + +const failedSet = new Set() + +const normalize = src => `${src || ''}`.replace(/&/g, '&') + +export function markImageFailed(src) { + if (src && !src.startsWith('data:')) { + failedSet.add(normalize(src)) + } +} + +export function unmarkImageFailed(src) { + failedSet.delete(normalize(src)) +} + +export function isImageFailed(src) { + return failedSet.size > 0 && failedSet.has(normalize(src)) +} diff --git a/resources/assets/sass/pages/components/dialog-wrapper.scss b/resources/assets/sass/pages/components/dialog-wrapper.scss index 666eda3ef..c29c06940 100644 --- a/resources/assets/sass/pages/components/dialog-wrapper.scss +++ b/resources/assets/sass/pages/components/dialog-wrapper.scss @@ -585,6 +585,13 @@ -webkit-overflow-scrolling: touch; } + img[data-failed-src] { + min-width: 36px; + min-height: 36px; + background: rgba(0, 0, 0, 0.06) url("data:image/svg+xml,%3Csvg viewBox='0 0 1024 1024' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M510.496 708.16H256.448c-25.504 0-46.176-18.032-46.176-40.288v-31.12l136.096-102.688 48.784 41.92c25.36 21.92 65.68 23.36 93.008 3.344l230.048-169.68 92.464 80.8c9.312 8.72 13.232 12.656 23.456 12.656 7.344 0 13.44-3.6 18.096-8.64 4.96-5.376 4.64-18.272 4.64-24.672V264.864c0-44.512-41.36-80.592-92.368-80.592H256.448c-51.008 0-92.368 36.08-92.368 80.592v403.008c0 21.376 9.728 41.888 27.056 56.992 17.328 15.104 40.816 23.616 65.312 23.616h253.568c11.808 0 22.656-8.368 23.504-18.656 0.96-11.808-9.712-21.632-22.992-21.632l-0.032-0.016zM210.272 264.88c0-22.24 20.672-40.288 46.176-40.288H764.48c25.504 0 46.176 18.032 46.192 40.288v168.656l-61.248-53.536c-17.12-12.64-42.176-12.688-59.36-0.112l-231.552 168.48c-9.12 6.608-22.48 6.144-30.912-1.088l-48.832-41.92c-17.2-14.624-44.384-15.392-62.624-1.76l-105.872 79.936V264.864z m120.752 94.48c0 11.952 3.568 23.648 10.256 33.584a60.8 60.8 0 0 0 27.328 22.272 61.264 61.264 0 0 0 66.336-13.104 60.064 60.064 0 0 0 13.2-65.888 60.544 60.544 0 0 0-22.416-27.12 61.184 61.184 0 0 0-76.88 7.52 60.416 60.416 0 0 0-17.824 42.72z m524.832 228.96a140.32 140.32 0 0 1 0 199.456c-55.504 55.088-145.408 55.12-200.88 0a140.544 140.544 0 0 1-40.72-99.488 140.56 140.56 0 0 1 41.6-99.12 142.56 142.56 0 0 1 99.824-41.312 142.56 142.56 0 0 1 100.16 40.448zM683.664 759.264a102.032 102.032 0 0 0 127.632 12.816l-140.544-139.616a100.224 100.224 0 0 0 12.912 126.752v0.048z m156.368-15.76a100.224 100.224 0 0 0-13.024-126.64 102.032 102.032 0 0 0-127.52-12.928l140.544 139.568z' fill='%23D9D9D9'%3E%3C/path%3E%3C/svg%3E") center / 36px no-repeat; + cursor: pointer; + } + .item-leave { pre, code,