2026-09-23 14:59:02 +08:00
|
|
|
|
<?php
|
|
|
|
|
|
/**
|
2026-09-23 16:22:01 +08:00
|
|
|
|
* 照片 / 附件灯箱:点页面里带 class="js-photo"(图片)或 "js-media"(任意附件)的链接,
|
|
|
|
|
|
* 就在当前页弹窗里预览,左右箭头(或键盘 ←/→、触摸滑动)切换,不再新开窗口。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 按链接的 data-kind 决定预览方式(没有 data-kind 视为 image):
|
|
|
|
|
|
* image —— <img> 大图
|
|
|
|
|
|
* video —— <video controls> 直接播放(media/show 支持 Range,可拖进度条)
|
|
|
|
|
|
* pdf —— <iframe> 内嵌(浏览器内建 PDF 阅读器)
|
|
|
|
|
|
* file —— 浏览器预览不了的(Word / ZIP 等):给出说明 + 下载按钮
|
2026-09-23 14:59:02 +08:00
|
|
|
|
*
|
|
|
|
|
|
* 用法:
|
2026-09-23 16:22:01 +08:00
|
|
|
|
* <a class="js-media" data-kind="video" href="<播放地址>" data-dl="<下载地址>" data-cap="名字">
|
|
|
|
|
|
* (data-dl 可选,file 视图的下载按钮用它;没有就退回 href + &dl=1)
|
|
|
|
|
|
* 在页面末尾 require 本模板(一份就够)。
|
2026-09-23 14:59:02 +08:00
|
|
|
|
*
|
|
|
|
|
|
* 依赖外部变量:无。
|
|
|
|
|
|
*/
|
|
|
|
|
|
?>
|
2026-09-23 16:22:01 +08:00
|
|
|
|
<div class="lightbox" id="lightbox" aria-hidden="true" role="dialog" aria-label="附件浏览">
|
2026-09-23 14:59:02 +08:00
|
|
|
|
<div class="lightbox-backdrop" data-lb-close="1"></div>
|
|
|
|
|
|
|
|
|
|
|
|
<button type="button" class="lightbox-nav lightbox-prev" data-lb-prev="1" aria-label="上一张">‹</button>
|
|
|
|
|
|
|
|
|
|
|
|
<figure class="lightbox-body">
|
2026-09-23 16:22:01 +08:00
|
|
|
|
<img class="lightbox-img" id="lightbox-img" alt="" hidden>
|
|
|
|
|
|
<video class="lightbox-video" id="lightbox-video" controls playsinline preload="metadata" hidden></video>
|
|
|
|
|
|
<iframe class="lightbox-frame" id="lightbox-frame" title="预览" hidden></iframe>
|
|
|
|
|
|
<div class="lightbox-file" id="lightbox-file" hidden>
|
|
|
|
|
|
<div class="lightbox-file-icon">📎</div>
|
|
|
|
|
|
<p class="lightbox-file-text" id="lightbox-file-name"></p>
|
|
|
|
|
|
<p class="lightbox-file-hint">这种格式浏览器打不开,点击下面按钮下载后查看</p>
|
|
|
|
|
|
<a class="btn-primary lightbox-file-dl" id="lightbox-file-dl" href="#">下载文件</a>
|
|
|
|
|
|
</div>
|
2026-09-23 14:59:02 +08:00
|
|
|
|
<figcaption class="lightbox-cap" id="lightbox-cap"></figcaption>
|
|
|
|
|
|
</figure>
|
|
|
|
|
|
|
|
|
|
|
|
<button type="button" class="lightbox-nav lightbox-next" data-lb-next="1" aria-label="下一张">›</button>
|
|
|
|
|
|
|
|
|
|
|
|
<button type="button" class="lightbox-close" data-lb-close="1" aria-label="关闭">×</button>
|
|
|
|
|
|
<div class="lightbox-count" id="lightbox-count"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
|
(function () {
|
|
|
|
|
|
'use strict';
|
|
|
|
|
|
|
2026-09-23 16:22:01 +08:00
|
|
|
|
var box = document.getElementById('lightbox');
|
2026-09-23 14:59:02 +08:00
|
|
|
|
if (!box) return;
|
2026-09-23 16:22:01 +08:00
|
|
|
|
var img = document.getElementById('lightbox-img');
|
|
|
|
|
|
var video = document.getElementById('lightbox-video');
|
|
|
|
|
|
var frame = document.getElementById('lightbox-frame');
|
|
|
|
|
|
var fileBox = document.getElementById('lightbox-file');
|
|
|
|
|
|
var fileName = document.getElementById('lightbox-file-name');
|
|
|
|
|
|
var fileDl = document.getElementById('lightbox-file-dl');
|
|
|
|
|
|
var cap = document.getElementById('lightbox-cap');
|
|
|
|
|
|
var count = document.getElementById('lightbox-count');
|
2026-09-23 14:59:02 +08:00
|
|
|
|
|
2026-09-23 16:22:01 +08:00
|
|
|
|
var items = []; // 打开时现收集页面上所有 js-photo / js-media(DOM 顺序)
|
2026-09-23 14:59:02 +08:00
|
|
|
|
var idx = 0;
|
|
|
|
|
|
|
|
|
|
|
|
function collect() {
|
2026-09-23 16:22:01 +08:00
|
|
|
|
return Array.prototype.slice.call(document.querySelectorAll('a.js-photo, a.js-media'));
|
2026-09-23 14:59:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function captionOf(a) {
|
|
|
|
|
|
return a.getAttribute('data-cap') || a.getAttribute('title')
|
|
|
|
|
|
|| (a.querySelector('img') ? a.querySelector('img').alt : '') || '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-23 16:22:01 +08:00
|
|
|
|
function kindOf(a) {
|
|
|
|
|
|
var k = a.getAttribute('data-kind');
|
|
|
|
|
|
return (k === 'video' || k === 'pdf' || k === 'file') ? k : 'image';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function downloadOf(a) {
|
|
|
|
|
|
return a.getAttribute('data-dl')
|
|
|
|
|
|
|| (a.getAttribute('href') || '').replace(/&dl=1$/, '') + '&dl=1';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function showView(kind) {
|
|
|
|
|
|
img.hidden = kind !== 'image';
|
|
|
|
|
|
video.hidden = kind !== 'video';
|
|
|
|
|
|
frame.hidden = kind !== 'pdf';
|
|
|
|
|
|
fileBox.hidden = kind !== 'file';
|
|
|
|
|
|
if (kind !== 'video') { video.pause(); video.removeAttribute('src'); video.load(); }
|
|
|
|
|
|
if (kind !== 'pdf') { frame.src = 'about:blank'; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-23 14:59:02 +08:00
|
|
|
|
function show(i) {
|
|
|
|
|
|
if (!items.length) return;
|
|
|
|
|
|
idx = (i + items.length) % items.length; // 循环切换
|
|
|
|
|
|
var a = items[idx];
|
2026-09-23 16:22:01 +08:00
|
|
|
|
var kind = kindOf(a);
|
2026-09-23 14:59:02 +08:00
|
|
|
|
var full = a.getAttribute('data-full') || a.getAttribute('href');
|
2026-09-23 16:22:01 +08:00
|
|
|
|
var capText = captionOf(a);
|
|
|
|
|
|
|
|
|
|
|
|
showView(kind);
|
|
|
|
|
|
if (kind === 'image') img.src = full;
|
|
|
|
|
|
else if (kind === 'video') { video.src = full; video.load(); }
|
|
|
|
|
|
else if (kind === 'pdf') frame.src = full;
|
|
|
|
|
|
else {
|
|
|
|
|
|
fileName.textContent = capText || '附件';
|
|
|
|
|
|
fileDl.href = downloadOf(a);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
img.alt = capText;
|
|
|
|
|
|
cap.textContent = capText;
|
2026-09-23 14:59:02 +08:00
|
|
|
|
count.textContent = items.length > 1 ? (idx + 1) + ' / ' + items.length : '';
|
2026-09-23 16:22:01 +08:00
|
|
|
|
|
|
|
|
|
|
// 预加载相邻两张图片(视频 / PDF 体积大,不预载)
|
2026-09-23 14:59:02 +08:00
|
|
|
|
[idx - 1, idx + 1].forEach(function (j) {
|
|
|
|
|
|
var na = items[(j + items.length) % items.length];
|
2026-09-23 16:22:01 +08:00
|
|
|
|
if (!na || kindOf(na) !== 'image') return;
|
2026-09-23 14:59:02 +08:00
|
|
|
|
var pre = new Image();
|
|
|
|
|
|
pre.src = na.getAttribute('data-full') || na.getAttribute('href');
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function openAt(anchor) {
|
|
|
|
|
|
items = collect();
|
|
|
|
|
|
var at = items.indexOf(anchor);
|
|
|
|
|
|
if (at < 0) { items = [anchor]; at = 0; }
|
|
|
|
|
|
box.classList.add('open');
|
|
|
|
|
|
box.setAttribute('aria-hidden', 'false');
|
|
|
|
|
|
document.body.style.overflow = 'hidden';
|
|
|
|
|
|
show(at);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function close() {
|
2026-09-23 16:22:01 +08:00
|
|
|
|
showView('none');
|
2026-09-23 14:59:02 +08:00
|
|
|
|
box.classList.remove('open');
|
|
|
|
|
|
box.setAttribute('aria-hidden', 'true');
|
|
|
|
|
|
document.body.style.overflow = '';
|
|
|
|
|
|
img.src = '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
document.addEventListener('click', function (ev) {
|
2026-09-23 16:22:01 +08:00
|
|
|
|
var link = ev.target.closest ? ev.target.closest('a.js-photo, a.js-media') : null;
|
2026-09-23 14:59:02 +08:00
|
|
|
|
if (link) {
|
|
|
|
|
|
ev.preventDefault();
|
|
|
|
|
|
openAt(link);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!box.classList.contains('open')) return;
|
|
|
|
|
|
if (ev.target.closest('[data-lb-prev]')) { show(idx - 1); return; }
|
|
|
|
|
|
if (ev.target.closest('[data-lb-next]')) { show(idx + 1); return; }
|
|
|
|
|
|
if (ev.target.closest('[data-lb-close]')) { close(); }
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
document.addEventListener('keydown', function (ev) {
|
|
|
|
|
|
if (!box.classList.contains('open')) return;
|
|
|
|
|
|
if (ev.key === 'Escape') close();
|
|
|
|
|
|
else if (ev.key === 'ArrowLeft') show(idx - 1);
|
|
|
|
|
|
else if (ev.key === 'ArrowRight') show(idx + 1);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 触摸滑动切图(手机上不用去够箭头)
|
|
|
|
|
|
var touchX = null, touchY = null;
|
|
|
|
|
|
box.addEventListener('touchstart', function (ev) {
|
|
|
|
|
|
if (ev.touches.length === 1) {
|
|
|
|
|
|
touchX = ev.touches[0].clientX;
|
|
|
|
|
|
touchY = ev.touches[0].clientY;
|
|
|
|
|
|
}
|
|
|
|
|
|
}, { passive: true });
|
|
|
|
|
|
box.addEventListener('touchend', function (ev) {
|
|
|
|
|
|
if (touchX === null || !ev.changedTouches.length) return;
|
|
|
|
|
|
var dx = ev.changedTouches[0].clientX - touchX;
|
|
|
|
|
|
var dy = ev.changedTouches[0].clientY - touchY;
|
|
|
|
|
|
if (Math.abs(dx) > 48 && Math.abs(dx) > Math.abs(dy) * 1.5) {
|
|
|
|
|
|
show(dx < 0 ? idx + 1 : idx - 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
touchX = touchY = null;
|
|
|
|
|
|
}, { passive: true });
|
|
|
|
|
|
})();
|
|
|
|
|
|
</script>
|