节点详情:照片灯箱浏览、参与人点击编辑、花销只记总计

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 含新规则。
This commit is contained in:
2026-09-23 14:59:02 +08:00
parent 86e0481a19
commit 48237c8947
8 changed files with 386 additions and 105 deletions
+125
View File
@@ -0,0 +1,125 @@
<?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="上一张">&#8249;</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="下一张">&#8250;</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>