(function(){ // 必须自己转义引号:textContent→innerHTML 只处理 & < >,插进 attr="…" 时引号会被用来逃逸出属性。 const escapeHtml=value=>String(value??'').replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"').replace(/'/g,'''); let overlay=null,items=[],index=0,restoreFocus=null,touchStartX=null; function build(){ overlay=document.createElement('div');overlay.className='image-viewer';overlay.hidden=true; overlay.setAttribute('role','dialog');overlay.setAttribute('aria-modal','true');overlay.setAttribute('aria-label','图片预览'); overlay.innerHTML='
'; document.body.append(overlay); overlay.querySelectorAll('[data-viewer-close]').forEach(node=>node.onclick=close); overlay.querySelector('.viewer-nav.prev').onclick=()=>step(-1); overlay.querySelector('.viewer-nav.next').onclick=()=>step(1); overlay.addEventListener('touchstart',event=>{touchStartX=event.changedTouches[0].clientX;},{passive:true}); overlay.addEventListener('touchend',event=>{if(touchStartX===null)return;const delta=event.changedTouches[0].clientX-touchStartX;touchStartX=null;if(Math.abs(delta)>60)step(delta<0?1:-1);},{passive:true}); document.addEventListener('keydown',event=>{ if(overlay.hidden)return; if(event.key==='Escape'){event.preventDefault();close();} else if(event.key==='ArrowLeft')step(-1); else if(event.key==='ArrowRight')step(1); else if(event.key==='Tab'){ // aria-modal 要求焦点留在对话框内,否则 Tab 会走到被遮罩盖住的背景控件上。 const focusable=[...overlay.querySelectorAll('button')].filter(node=>!node.hidden&&node.offsetParent!==null); if(!focusable.length)return; const first=focusable[0],last=focusable[focusable.length-1]; if(event.shiftKey&&document.activeElement===first){event.preventDefault();last.focus();} else if(!event.shiftKey&&document.activeElement===last){event.preventDefault();first.focus();} else if(!overlay.contains(document.activeElement)){event.preventDefault();first.focus();} } }); } function show(){ const item=items[index]||{},image=overlay.querySelector('img'),state=overlay.querySelector('.viewer-state'); image.removeAttribute('src'); // 置空字符串会让部分浏览器重新请求当前页面地址 image.alt=item.original_name?`成品图:${item.original_name}`:'实验成品图'; image.hidden=true;state.hidden=false;state.textContent='正在载入…'; image.onload=()=>{image.hidden=false;state.hidden=true;}; image.onerror=()=>{image.hidden=true;state.hidden=false;state.textContent='图片加载失败,可能已被删除';}; if(item.url)image.src=item.url;else image.onerror(); overlay.querySelector('figcaption').textContent=[item.original_name,item.width&&item.height?`${item.width}×${item.height}`:''].filter(Boolean).join(' · '); overlay.querySelector('.viewer-counter').textContent=items.length>1?`${index+1} / ${items.length}`:''; overlay.querySelectorAll('.viewer-nav').forEach(node=>node.hidden=items.length<2); } function step(direction){if(items.length<2)return;index=(index+direction+items.length)%items.length;show();} function open(list,start=0){ if(!list?.length)return; if(!overlay)build(); items=list;index=Math.max(0,Math.min(start,list.length-1)); restoreFocus=document.activeElement; document.documentElement.classList.add('viewer-open'); overlay.hidden=false;show(); overlay.querySelector('.viewer-close').focus(); } function close(){ if(!overlay||overlay.hidden)return; overlay.hidden=true;overlay.querySelector('img').removeAttribute('src'); document.documentElement.classList.remove('viewer-open'); if(restoreFocus?.focus)restoreFocus.focus(); restoreFocus=null; } function galleryHtml(images,emptyText='本条记录暂未上传图片'){ if(!images?.length)return ``; // 不用 loading="lazy":缩略图是在隐藏的详情弹窗里解析的,惰性加载会一直不触发;单张只有几 KB,最多 6 张。 return ``; } function wire(root,images){ if(!root||!images?.length)return; root.querySelectorAll('[data-viewer-index]').forEach(button=>{ button.onclick=()=>open(images,Number(button.dataset.viewerIndex)); const image=button.querySelector('img'); if(image)image.onerror=()=>{image.hidden=true;button.querySelector('.gallery-fallback').hidden=false;}; }); } function render(root,images,emptyText){if(!root)return;root.innerHTML=galleryHtml(images,emptyText);wire(root,images);} window.ImageViewer={open,close,galleryHtml,wire,render}; })();