Files
TripLog/templates/nodes/_members.php
T
laoyang 48237c8947 节点详情:照片灯箱浏览、参与人点击编辑、花销只记总计
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 含新规则。
2026-09-23 14:59:02 +08:00

283 lines
14 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 节点参与人面板
* 依赖外部变量:$node、$members、$familyUsers、$roleTags(id=>name)、$emotions(emoji=>中文)
*
* 编辑走弹窗:点某个参与人旁的「编辑」,弹出预填好的表单(角色 / 心情 / 签到时间),
* 提交目标仍是 members/store —— Members::save() 本来就是 upsert(同一节点同一人一条),
* 所以「编辑已加入的人」不需要任何新接口。
*/
$members = $members ?? [];
$familyUsers = $familyUsers ?? [];
$roleTags = $roleTags ?? [];
$emotions = $emotions ?? [];
$joinedIds = [];
foreach ($members as $m) {
$joinedIds[(int)$m['user_id']] = true;
}
$notJoined = [];
foreach ($familyUsers as $u) {
if (!isset($joinedIds[(int)$u['id']])) $notJoined[] = $u;
}
/* 上一次提交的原始值(flash_old 存的)只用来回填「刚才没保存成功的那个人」的表单,
按 user_id 精确匹配,避免污染其他参与人的编辑弹窗。 */
$mOldFilled = !empty($_SESSION['_old']);
$mOldUid = (string)old('user_id', '');
?>
<section class="panel" id="members">
<h2 class="section-title">👥 参与人 (<?= count($members) ?>)</h2>
<?php if (empty($members)): ?>
<p class="muted">还没有添加参与人</p>
<?php else: ?>
<div class="member-list">
<?php foreach ($members as $m):
$mName = (string)($m['name'] ?? '');
if ($mName === '') $mName = '已注销用户';
$mUid = (int)$m['user_id'];
$mUseOld = $mOldFilled && $mOldUid === (string)$mUid;
?>
<div class="member-row">
<?php if (!empty($m['avatar'])): ?>
<img src="<?= e($m['avatar']) ?>" alt="" class="avatar-sm">
<?php else: ?>
<span class="avatar-sm avatar-placeholder"><?= e(mb_substr($mName, 0, 1)) ?></span>
<?php endif; ?>
<span class="member-name"><?= e($mName) ?></span>
<?php foreach ($m['role_tag_names'] as $tagName): ?>
<span class="member-role"><?= e($tagName) ?></span>
<?php endforeach; ?>
<?php if (!empty($m['emotion'])): ?>
<span class="member-emotion" title="<?= e($emotions[$m['emotion']] ?? '') ?>">
<?= e($m['emotion']) ?><?= e($emotions[$m['emotion']] ?? '') ?>
</span>
<?php endif; ?>
<?php if (!empty($m['check_in_time'])): ?>
<span class="member-checkin">✓ 签到 <?= date('m-d H:i', strtotime((string)$m['check_in_time'])) ?></span>
<?php endif; ?>
<button type="button" class="member-edit" data-edit-member="<?= $mUid ?>"
title="修改 <?= e($mName) ?> 的角色 / 心情 / 签到">编辑</button>
<form method="post" action="<?= url('members/destroy') ?>" class="member-del-form"
onsubmit="return confirm('把 <?= e($mName) ?> 从本节点移出?');">
<input type="hidden" name="_csrf" value="<?= e(csrf_token()) ?>">
<input type="hidden" name="node_id" value="<?= (int)$node['id'] ?>">
<input type="hidden" name="user_id" value="<?= $mUid ?>">
<button type="submit" class="comment-del">移出</button>
</form>
</div>
<?php /* ── 该参与人的编辑弹窗(预填当前值) ───────────────── */ ?>
<div class="modal" id="member-edit-<?= $mUid ?>" aria-hidden="true" role="dialog"
aria-label="编辑参与人 <?= e($mName) ?>">
<div class="modal-backdrop" data-close-member="1"></div>
<div class="modal-content">
<h3 class="modal-title">编辑参与人:<?= e($mName) ?></h3>
<p class="modal-hint">改完点「保存」即可,同一人只会有一条参与记录。</p>
<form method="post" action="<?= url('members/store') ?>" class="form form-plain">
<input type="hidden" name="_csrf" value="<?= e(csrf_token()) ?>">
<input type="hidden" name="node_id" value="<?= (int)$node['id'] ?>">
<input type="hidden" name="user_id" value="<?= $mUid ?>">
<?php if (!empty($roleTags)): ?>
<div class="form-row">
<label>角色标签(可多选)</label>
<div class="check-grid">
<?php $eSelTags = $mUseOld ? old_array('role_tag_ids') : array_map('strval', $m['role_tag_ids'] ?? []); ?>
<?php foreach ($roleTags as $tagId => $tagName): ?>
<label class="checkbox-label">
<input type="checkbox" name="role_tag_ids[]" value="<?= (int)$tagId ?>"
<?= in_array((string)(int)$tagId, $eSelTags, true) ? ' checked' : '' ?>>
<span><?= e($tagName) ?></span>
</label>
<?php endforeach; ?>
</div>
</div>
<?php else: ?>
<p class="muted" style="font-size:12px;">
还没有角色标签,去
<a href="<?= url('role_tags/index') ?>">角色标签管理</a>
先建几个(例如:司机 / 摄影师 / 记账员)
</p>
<?php endif; ?>
<div class="form-row">
<label>心情(可选)</label>
<div class="emotion-grid">
<?php $eSelEmotion = $mUseOld ? (string)old('emotion', '') : (string)($m['emotion'] ?? ''); ?>
<label class="emotion-option">
<input type="radio" name="emotion" value=""<?= $eSelEmotion === '' ? ' checked' : '' ?>>
<span class="emotion-face">🚫</span>
<span class="emotion-text">不记录</span>
</label>
<?php foreach ($emotions as $emoji => $label): ?>
<label class="emotion-option" title="<?= e($label) ?>">
<input type="radio" name="emotion" value="<?= e($emoji) ?>"<?= $eSelEmotion === (string)$emoji ? ' checked' : '' ?>>
<span class="emotion-face"><?= e($emoji) ?></span>
<span class="emotion-text"><?= e($label) ?></span>
</label>
<?php endforeach; ?>
</div>
</div>
<div class="form-row">
<label>签到时间(可选,留空表示未签到)</label>
<?php
$dtName = 'check_in_time';
$dtValue = $mUseOld ? old_dt('check_in_time', null) : ($m['check_in_time'] ?? null);
$dtRequired = false;
$dtId = 'check-in-date-' . $mUid;
require __DIR__ . '/../_datetime.php';
?>
</div>
<div class="form-actions">
<button type="submit" class="btn-primary">保存</button>
<button type="button" class="btn btn-sm" data-close-member="1">取消</button>
</div>
</form>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if (!empty($notJoined)): ?>
<form method="post" action="<?= url('members/addAll') ?>" style="display:inline"
onsubmit="return confirm('把家庭里的 <?= count($familyUsers) ?> 位成员全部加入这个节点?');">
<input type="hidden" name="_csrf" value="<?= e(csrf_token()) ?>">
<input type="hidden" name="node_id" value="<?= (int)$node['id'] ?>">
<button type="submit" class="btn-sm">+ 一键加入全家(<?= count($notJoined) ?> 人待加入)</button>
</form>
<?php endif; ?>
<details class="inline-panel">
<summary>+ 添加参与人</summary>
<form method="post" action="<?= url('members/store') ?>" class="form form-plain"
data-old="<?= $mOldFilled ? '1' : '0' ?>">
<input type="hidden" name="_csrf" value="<?= e(csrf_token()) ?>">
<input type="hidden" name="node_id" value="<?= (int)$node['id'] ?>">
<div class="form-row">
<label>成员 *</label>
<select name="user_id" required>
<option value="">— 请选择 —</option>
<?php $mSelUser = (string)old('user_id', ''); ?>
<?php foreach ($familyUsers as $u): ?>
<option value="<?= (int)$u['id'] ?>"<?= $mSelUser === (string)(int)$u['id'] ? ' selected' : '' ?>>
<?= e($u['name']) ?><?= isset($joinedIds[(int)$u['id']]) ? '(已加入,保存将覆盖)' : '' ?>
</option>
<?php endforeach; ?>
</select>
</div>
<?php if (!empty($roleTags)): ?>
<div class="form-row">
<label>角色标签(可多选)</label>
<div class="check-grid">
<?php $mSelTags = old_array('role_tag_ids'); ?>
<?php foreach ($roleTags as $tagId => $tagName): ?>
<label class="checkbox-label">
<input type="checkbox" name="role_tag_ids[]" value="<?= (int)$tagId ?>"
<?= in_array((string)(int)$tagId, $mSelTags, true) ? ' checked' : '' ?>>
<span><?= e($tagName) ?></span>
</label>
<?php endforeach; ?>
</div>
</div>
<?php else: ?>
<p class="muted" style="font-size:12px;">
还没有角色标签,去
<a href="<?= url('role_tags/index') ?>">角色标签管理</a>
先建几个(例如:司机 / 摄影师 / 记账员)
</p>
<?php endif; ?>
<div class="form-row">
<label>心情(可选)</label>
<div class="emotion-grid">
<?php $mSelEmotion = (string)old('emotion', ''); ?>
<label class="emotion-option">
<input type="radio" name="emotion" value=""<?= $mSelEmotion === '' ? ' checked' : '' ?>>
<span class="emotion-face">🚫</span>
<span class="emotion-text">不记录</span>
</label>
<?php foreach ($emotions as $emoji => $label): ?>
<label class="emotion-option" title="<?= e($label) ?>">
<input type="radio" name="emotion" value="<?= e($emoji) ?>"<?= $mSelEmotion === (string)$emoji ? ' checked' : '' ?>>
<span class="emotion-face"><?= e($emoji) ?></span>
<span class="emotion-text"><?= e($label) ?></span>
</label>
<?php endforeach; ?>
</div>
</div>
<div class="form-row">
<label>签到时间(可选,留空表示未签到)</label>
<?php $dtName = 'check_in_time'; $dtValue = old_dt('check_in_time', null); $dtRequired = false; $dtId = 'check-in-date'; ?>
<?php require __DIR__ . '/../_datetime.php'; ?>
</div>
<div class="form-actions">
<button type="submit" class="btn-primary">保存</button>
</div>
</form>
</details>
</section>
<script>
(function () {
'use strict';
var openId = null;
function modalOf(uid) {
return document.getElementById('member-edit-' + uid);
}
function openModal(uid) {
var modal = modalOf(uid);
if (!modal) return;
if (openId !== null) closeModal(openId);
openId = uid;
modal.classList.add('open');
modal.setAttribute('aria-hidden', 'false');
document.body.style.overflow = 'hidden';
}
function closeModal(uid) {
var modal = modalOf(uid);
if (!modal) return;
modal.classList.remove('open');
modal.setAttribute('aria-hidden', 'true');
document.body.style.overflow = '';
if (openId === uid) openId = null;
}
document.addEventListener('click', function (ev) {
var opener = ev.target.closest ? ev.target.closest('[data-edit-member]') : null;
if (opener) {
ev.preventDefault();
openModal(opener.getAttribute('data-edit-member'));
return;
}
var closer = ev.target.closest ? ev.target.closest('[data-close-member]') : null;
if (closer && openId !== null) {
ev.preventDefault();
closeModal(openId);
}
});
document.addEventListener('keydown', function (ev) {
if (ev.key === 'Escape' && openId !== null) closeModal(openId);
});
})();
</script>