1) 照片与附件不再新开窗口看 之前点缩略图直接 target=_blank 打开原始文件,看完还得切回来。 现在统一走当前页灯箱(templates/_lightbox.php,详情页照片墙与 照片管理页共用):左右箭头 / 键盘 ←→ / 手机滑动切上一张下一张, Esc 或点背景关闭,带「第几张 / 共几张」计数并预加载相邻两张。 非图片附件(PDF 等)仍保留直接打开与下载。 2) 参与人可以直接改了 之前加完只能移出重加。现在每个参与人旁有「编辑」,点开预填弹窗 (角色标签 / 心情 / 签到时间),保存仍走 members/store —— Members::save() 本来就是 upsert(同节点同人一条),服务端零改动。 失败回填按 user_id 精确匹配,只回填刚才没保存成的那个人。 3) 花销去掉预算,只记真实花费 明细上移,底部一行「总计(N 笔)¥X」。表单不再有「预算」选项, 查询一律 is_budget=0(历史预算行不再出现在列表与合计里); 活动页统计同步改成「共 N 笔 · 总计」。列保留在库里不删数据。 已验证:本地全链 11 步全绿(php -l / 渲染 286 项 / SQL / PDO / CSS / Mime / amap / 内联 JS×11 / 布局几何 / 表单交互 73 项);远端 e2e 296 项 0 失败(含灯箱、编辑弹窗、历史预算行排除的专项断言); 部署后双 PHP 渲染逐字节一致、60 文件哈希一致,线上 style.css 含新规则。
126 lines
4.6 KiB
PHP
126 lines
4.6 KiB
PHP
<?php
|
||
/**
|
||
* 照片灯箱(Lightbox):点页面里带 class="js-photo" 的链接,就在当前页弹窗里看大图,
|
||
* 左右箭头(或键盘 ←/→、触摸滑动)切上一张/下一张,不再新开窗口。
|
||
*
|
||
* 用法:
|
||
* 1. 图片链接写成 <a class="js-photo" href="<大图地址>" data-cap="<说明文字>">…</a>
|
||
* (没有 data-cap 时回落到 title / alt / href)
|
||
* 2. 在页面末尾 require 本模板(一份就够,多页共用同一份标记与脚本)
|
||
*
|
||
* 依赖外部变量:无。
|
||
*/
|
||
?>
|
||
<div class="lightbox" id="lightbox" aria-hidden="true" role="dialog" aria-label="图片浏览">
|
||
<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">
|
||
<img class="lightbox-img" id="lightbox-img" alt="">
|
||
<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';
|
||
|
||
var box = document.getElementById('lightbox');
|
||
if (!box) return;
|
||
var img = document.getElementById('lightbox-img');
|
||
var cap = document.getElementById('lightbox-cap');
|
||
var count = document.getElementById('lightbox-count');
|
||
|
||
var items = []; // 打开时现收集页面上所有 js-photo(DOM 顺序)
|
||
var idx = 0;
|
||
|
||
function collect() {
|
||
return Array.prototype.slice.call(document.querySelectorAll('a.js-photo'));
|
||
}
|
||
|
||
function captionOf(a) {
|
||
return a.getAttribute('data-cap') || a.getAttribute('title')
|
||
|| (a.querySelector('img') ? a.querySelector('img').alt : '') || '';
|
||
}
|
||
|
||
function show(i) {
|
||
if (!items.length) return;
|
||
idx = (i + items.length) % items.length; // 循环切换
|
||
var a = items[idx];
|
||
var full = a.getAttribute('data-full') || a.getAttribute('href');
|
||
img.alt = captionOf(a);
|
||
cap.textContent = captionOf(a);
|
||
count.textContent = items.length > 1 ? (idx + 1) + ' / ' + items.length : '';
|
||
img.src = full;
|
||
// 预加载相邻两张,切换不等待
|
||
[idx - 1, idx + 1].forEach(function (j) {
|
||
var na = items[(j + items.length) % items.length];
|
||
if (!na) return;
|
||
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() {
|
||
box.classList.remove('open');
|
||
box.setAttribute('aria-hidden', 'true');
|
||
document.body.style.overflow = '';
|
||
img.src = '';
|
||
}
|
||
|
||
document.addEventListener('click', function (ev) {
|
||
var link = ev.target.closest ? ev.target.closest('a.js-photo') : null;
|
||
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>
|