(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 `${escapeHtml(emptyText)}
`;
// 不用 loading="lazy":缩略图是在隐藏的详情弹窗里解析的,惰性加载会一直不触发;单张只有几 KB,最多 6 张。
return `${images.map((item,position)=>`
`).join('')}
`;
}
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};
})();