v11: 家谱按户排布(夫妻并排/长幼排序/父母括号/姻亲);观影设置下拉收拢影院·搜索·统计·年度报告;观影人头像

家谱(用户第 2 点,重写模板与 Kinship::tree):
- 同代聚成「夫妻单元」(户):血亲当主位、配偶并排在其右,兄弟_姐妹按长幼(长子左)
- 血亲可达性判「姻亲」:只走 parent/child 边从「我」可达者为本家血亲,妻子的父母
  走不到 → 标「姻亲」并排到本代末尾(旧判据「上无父母且下无子女」对岳父母恒为假,是真 bug)
- 父母括号连线:横杆盖住父母列到其子女各列,竖线落父母列中心、每子女列一道下线
- 称呼按长幼定死:姐夫/妹夫、嫂子/弟媳、大/小舅子、大/小姨子、大伯子/小叔子、
  大姑子/小姑子、侄子/侄女、外甥/外甥女、堂/表
- 列宽地板 minmax(min-content,1fr):原 minmax(0,1fr) 在窄屏+户数多时把列压到卡片
  (96px)以下 → 同代卡片互相重叠;.tnode 加 flex:0 0 auto 防被 flex 压扁

观影(用户第 3~5 点):
- 顶栏删「影院」入口(影院是观影模块的子功能);观影列表右侧加「⚙ 设置」下拉,
  内含 影院管理 / 高级搜索 / 统计 / 年度报告(后三者从 MovieLog 迁入,
  app/MovieStats.php + 三个只读路由,MySQL 方言;图表全用纯 CSS,不引 CDN)
- 评价区删除「我」的长条徽标(被 flex column stretch 拉成整行宽的丑色带)
- 观影人弹窗列表与已选 chips 带头像;syncChips 点名取 .att-name/.att-av
  (旧写法取「第一个 span」,v11 起那是隐藏占位,chip 上会没名字)

验证:e2e 432/0、桌面几何 268/0、移动几何 464/0、交互几何 97/0、
静态空跑 199/0、反向验证 v11 十条 10抓/0漏。本轮无 DB 变更(Schema 仍 v10)。
移动几何抓出两个真溢出并已修:高级搜索页 select 长选项撑破 .field、
原生 date 输入撑破 .date-range(360/375px)。
This commit is contained in:
2026-10-01 12:11:43 +08:00
parent 2c36335c3e
commit e9b3b8666b
12 changed files with 1494 additions and 103 deletions
+21 -5
View File
@@ -97,12 +97,13 @@ $selAttendees = array_map('intval', (array)old('attendees', $curAttendees));
<label>同行的人</label>
<?php /* v10:名册人多时 checkbox 网格会拉得很长 —— 改为「chips + 弹窗选择」。
attendees[] 的 checkbox 仍保留(在弹窗列表里,选中即提交),无 JS 时
借 noscript 样式把弹窗平铺成普通勾选列表,提交语义完全不变。 */ ?>
借 noscript 样式把弹窗平铺成普通勾选列表,提交语义完全不变。
v11:chips 与弹窗列表都带上头像($p 来自 Person::roster,含 p.avatar)。 */ ?>
<?php if ($people): ?>
<div class="att-chips" id="attChips">
<?php foreach ($people as $p): ?>
<?php if (in_array((int)$p['id'], $selAttendees, true)): ?>
<span class="att-chip" data-id="<?= (int)$p['id'] ?>"><?= e($p['shown'] ?? $p['name']) ?><button type="button" class="att-x" data-id="<?= (int)$p['id'] ?>" title="移除">×</button></span>
<span class="att-chip" data-id="<?= (int)$p['id'] ?>"><img src="<?= e(avatar_url($p['avatar'] ?? null)) ?>" alt=""><span class="att-name"><?= e($p['shown'] ?? $p['name']) ?></span><button type="button" class="att-x" data-id="<?= (int)$p['id'] ?>" title="移除">×</button></span>
<?php endif; ?>
<?php endforeach; ?>
</div>
@@ -122,7 +123,8 @@ $selAttendees = array_map('intval', (array)old('attendees', $curAttendees));
data-shown="<?= e((string)($p['shown'] ?? '')) ?>">
<input type="checkbox" name="attendees[]" value="<?= (int)$p['id'] ?>"
<?= in_array((int)$p['id'], $selAttendees, true) ? 'checked' : '' ?>>
<span><?= e($p['shown'] ?? $p['name']) ?></span>
<img class="att-av" src="<?= e(avatar_url($p['avatar'] ?? null)) ?>" alt="" loading="lazy">
<span class="att-name"><?= e($p['shown'] ?? $p['name']) ?></span>
<span class="att-initials" hidden></span>
</label>
<?php endforeach; ?>
@@ -233,12 +235,26 @@ $selAttendees = array_map('intval', (array)old('attendees', $curAttendees));
chips.innerHTML = '';
Array.prototype.forEach.call(list.querySelectorAll('.att-item input:checked'), function (box) {
var item = box.closest('.att-item');
var label = item.querySelector('span').textContent;
// ⚠️ 必须点名取 .att-name:v11 起 .att-item 里第一个元素是头像 <img>,
// 早先靠「取第一个 span 当名字」碰巧成立,现在第一个 span 是隐藏的
// 首字母占位,选完人 chip 上就没名字了。
var nameEl = item.querySelector('.att-name');
var label = nameEl ? nameEl.textContent : box.value;
var av = item.querySelector('.att-av');
var id = box.value;
var chip = document.createElement('span');
chip.className = 'att-chip';
chip.setAttribute('data-id', id);
chip.textContent = label;
if (av) {
var im = document.createElement('img');
im.src = av.getAttribute('src'); // 服务端已 e() 过,这里原样搬
im.alt = '';
chip.appendChild(im);
}
var nm = document.createElement('span');
nm.className = 'att-name';
nm.textContent = label;
chip.appendChild(nm);
var x = document.createElement('button');
x.type = 'button'; x.className = 'att-x'; x.textContent = '×';
x.title = '移除'; x.setAttribute('data-id', id);