/** * 全局附件预览(v24 用户第 3 条;第 19 批 第 6 条加左右翻页) * * 点缩略图 → **在本页**弹一层把内容放出来,不再新开标签页。 * · 图片 → * · 视频 → (media.php 支持 Range,进度条能拖) * · PDF → (用浏览器自带的阅读器) * · 其它(Word / Excel / …)→ 根本不进这里:那段链接指向的是下载地址,点了就下。 * * 右上角恒有一个「下载」按钮(用户原话:所有的预览页面,右上角都有一个下载按钮)。 * * 触发方式是数据属性,不用给每个地方写一遍初始化: * * * ──────────────────────────────────────────────────────────────── * v24 第 19 批 用户第 6 条:「行程中的图片和视频,预览的时候,左右有箭头, * 可以直接查看下一张。」 * * 做法:翻页的候选 = **同一页里所有可预览的附件**(`[data-prev]`),按文档顺序。 * 为什么不做"只在本段行程内翻页": * · 这个预览层是**全站一个**(图片/视频/PDF 共用),而 attachments 墙是按 * 宿主分块渲染的;要"按块"就得给每块再编一次组号,收益很小、复杂度不小; * · 用户要的行为是"看完这张接着看下一张",同页顺序翻页完全满足, * 并且跨段也不会跳去别的页面 —— 翻到下一个旅程节点时用户能立刻看出来。 * ⚠️ 只有**一个**可预览项时箭头不显示(点了没反应的元素比没有更糟)。 * ⚠️ 键盘 ← → 也翻页;视频/PDF 里方向键有自己的含义,所以**只在图片**上接管方向键。 */ (function () { 'use strict'; var box = document.getElementById('pv'); if (!box) { return; } var stage = box.querySelector('.pv-stage'); var titleEl = box.querySelector('.pv-name'); var metaEl = box.querySelector('.pv-meta'); var dlEl = box.querySelector('.pv-dl'); var closeEl = box.querySelector('.pv-close'); var prevEl = box.querySelector('.pv-prev'); var nextEl = box.querySelector('.pv-next'); var opener = null; /** 当前这一层里能看到的所有可预览项(文档顺序),以及"正在看第几个" */ var items = []; var cur = -1; function clear() { // ⚠️ 换内容要**整个重建**节点,不能只改 src: // 改 src 不会自动卸载上一支(声音会叠着放), 同理。 stage.innerHTML = ''; } /** 收一份当前页面里所有可预览的附件(每次打开都重收,页面内容可能被改过) */ function collect(trigger) { items = Array.prototype.slice.call(document.querySelectorAll('[data-prev]')); cur = items.indexOf(trigger); if (cur < 0) { items = [trigger]; cur = 0; } } /** 第一张 / 最后一张时箭头要禁用(循环翻页会让人不知道自己在哪儿) */ function syncArrows() { if (!prevEl || !nextEl) { return; } var many = items.length > 1; prevEl.hidden = !many; nextEl.hidden = !many; prevEl.disabled = !many || cur <= 0; nextEl.disabled = !many || cur >= items.length - 1; } function paint(trigger) { var kind = trigger.getAttribute('data-prev') || 'image'; var src = trigger.getAttribute('data-src') || trigger.getAttribute('href') || ''; var dl = trigger.getAttribute('data-dl') || src; if (!src) { return false; } opener = trigger; clear(); var el; if (kind === 'video') { el = document.createElement('video'); el.className = 'pv-video'; el.src = src; el.controls = true; el.autoplay = true; el.playsInline = true; } else if (kind === 'pdf') { el = document.createElement('iframe'); el.className = 'pv-pdf'; el.src = src; // ⚠️ 这里**绝对不能**加 sandbox 属性。加上去 PDF 会直接打不开: // 页面只剩一行「此页面已被 Chromium 屏蔽」,阅读器根本不起。 // // 实测(Chromium 153,五组同源对照): // · 无 sandbox → 正常渲染(多挂一个内置阅读器扩展 frame) // · allow-same-origin allow-scripts → 屏蔽 // · allow-scripts → 屏蔽 // · + allow-popups-to-escape-sandbox → **仍然屏蔽**(网上很多答案说这个能救,本版本不行) // · + allow-forms allow-popups → 仍然屏蔽 // ⇒ 只要属性存在就不给 PDF 插件权限。加白名单无效,只能不加。 // // 不加也安全:PDF 由浏览器内置阅读器(mhjfbmdgcfjbbpaeojofohoefgiehjai) // 在**独立扩展进程**里渲染,不是普通页面上下文,拿不到父页的 window / DOM / cookie。 // PDF 是原生文档、不是 HTML,本就没有能访问父页面的脚本环境。 // 服务端另有 `X-Content-Type-Options: nosniff` + MIME 按内容嗅探(media.php), // PDF 不会被当 HTML 执行。若将来真需要禁止 PDF 内嵌 JS,正确做法是换 pdf.js 自渲染, // 而不是给 iframe 挂 sandbox——那会把整个 PDF 预览一起废掉。 } else { el = document.createElement('img'); el.className = 'pv-img'; el.src = src; el.alt = ''; } stage.appendChild(el); titleEl.textContent = trigger.getAttribute('data-name') || ''; var sz = trigger.getAttribute('data-size') || ''; var counter = items.length > 1 ? (' ' + (cur + 1) + '/' + items.length) : ''; metaEl.textContent = sz + counter; dlEl.setAttribute('href', dl); // 下载要带上真实文件名:download 属性 + 服务端 Content-Disposition 双保险 var nm = trigger.getAttribute('data-name') || ''; if (nm) { dlEl.setAttribute('download', nm); } else { dlEl.removeAttribute('download'); } syncArrows(); return true; } function open(trigger) { collect(trigger); if (!paint(trigger)) { return; } box.hidden = false; box.setAttribute('aria-hidden', 'false'); closeEl.focus(); } /** * 翻到上一张 / 下一张。 * @param {number} dir -1 = 上一张,+1 = 下一张 */ function step(dir) { if (items.length <= 1) { return; } var n = cur + dir; if (n < 0 || n >= items.length) { return; } cur = n; paint(items[cur]); } function close() { box.hidden = true; box.setAttribute('aria-hidden', 'true'); clear(); // 停掉视频 / 卸掉 PDF if (opener && opener.focus) { opener.focus(); } opener = null; items = []; cur = -1; } /** 正在看的是不是图片(视频/PDF 里方向键另有含义,不能抢) */ function isImage() { return items[cur] && (items[cur].getAttribute('data-prev') || '') === 'image'; } document.addEventListener('click', function (e) { var t = e.target && e.target.closest ? e.target.closest('[data-prev]') : null; if (t) { // ⚠️ 只拦"可预览"的:Word/Excel 那类链接没有 data-prev, // 浏览器照常走 href(就是下载地址),不会被这里挡住。 e.preventDefault(); open(t); return; } // 点遮罩(stage 本身或弹层空白处)关闭;点内容不关 if (!box.hidden && (e.target === box || e.target === stage)) { close(); } }); if (prevEl) { prevEl.addEventListener('click', function () { step(-1); }); } if (nextEl) { nextEl.addEventListener('click', function () { step(1); }); } closeEl.addEventListener('click', close); document.addEventListener('keydown', function (e) { if (box.hidden) { return; } if (e.key === 'Escape') { close(); return; } if (e.key === 'ArrowLeft' && isImage()) { e.preventDefault(); step(-1); return; } if (e.key === 'ArrowRight' && isImage()) { e.preventDefault(); step(1); } }); })();