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
+126 -27
View File
@@ -1,13 +1,26 @@
<?php
/**
* 我的家谱:以我为中心,向上向下各三代。
* 我的家谱:以我为中心,向上向下各三代。**v11 重写** —— 从「按代平铺」改成「按户排布」。
*
* 只做「呈现 + 说清楚推导规则」两件事 —— 关系的录入在成员详情页,
* 因为那里有具体的人,比在一张图上点来点去更好理解。
* 用户提出的四点,对应这里的四个做法:
* ① 「每一层级都需要按照长子左、次子右的顺序排序,夫妻要放在一起」
* → 同一代不再平铺一堆卡片,而是先聚成**夫妻单元**(一户),
* 夫妻并排(男左女右、血亲在左),再把单元按长幼从左到右排。
* 聚合与排序在 App\Kinship::tree() 里算好(['units']),这里只负责画。
* ② 「上下代之间要有连线,明确谁是谁的父母与孩子」
* → 用**父母括号**(family bracket):父母那一户下方画一个横跨它所有子女的
* 方括号,括号中心垂下一根线接到父母,每个子女各自的竖线接进同一根横杆。
* 谁是谁的父母/孩子因此一眼可辨,而不是"两代之间一条装饰竖线"。
* ③ 「妹夫、姐夫……谁是姐姐谁是妹妹是很明确的事情」等称呼
* → 全部在 Kinship::label() 里按长幼定死(本模板只渲染结果,不自己做判断)。
* ④ 「小姨子、小姑子、大姨子这种称呼,有错的,需要完全正确」
* → 同上;另对「妻子的父母」这类漂在图上的旁支标一句「姻亲」,免得看着像断线。
*/
$gens = $tree['generations'];
$nodes = $tree['nodes'];
$me = $nodes[$meId] ?? null;
$gens = $tree['generations'];
$units = $tree['units'] ?? [];
$unitName = $tree['unit_names'] ?? [];
$nodes = $tree['nodes'];
$me = $nodes[$meId] ?? null;
// 代数 → 人类说法
$genWord = function (int $level): string {
@@ -22,10 +35,24 @@ $genWord = function (int $level): string {
$down = [1 => '子女辈', 2 => '孙辈', 3 => '曾孙辈'];
return $down[$level] ?? '下 ' . $level . ' 代';
};
$levels = array_keys($gens);
// 整棵图的宽度取「最宽那一代」—— 所有代共用一个宽度,连线才能跟人对齐
$maxUnits = 1;
foreach ($levels as $lv) {
$maxUnits = max($maxUnits, count($units[$lv] ?? []));
}
// ⚠️ 列宽最小值必须是 min-content(**不能**是 0):
// v11 实测踩到 —— 最小值给 0 时列会被压到 96px 以下,而卡片是固定 96px 宽,
// 于是窄屏 + 户数多时同代卡片**互相重叠**(照片与名字糊成一团)。
// minmax(min-content, 1fr) 给列一个「不小于内容」的地板:内容窄时照旧均分填满,
// 内容宽过版心时整棵树交给 .tree-inner 横向滚动(这正是家谱的设计意图)。
$colStyle = 'grid-template-columns: repeat(' . (int)$maxUnits . ', minmax(min-content, 1fr));';
?>
<div class="page-head">
<h1>我的家谱</h1>
<span class="sub">同代按长幼从左到右 · 夫妻并排 · 括号表示谁是谁的父母</span>
</div>
<?php if (!$nodes || count($nodes) <= 1): ?>
@@ -37,28 +64,100 @@ $genWord = function (int $level): string {
<?php else: ?>
<div class="tree">
<?php foreach ($gens as $level => $ids): ?>
<div class="tree-gen">
<div class="tree-gen-label">
<span class="gname"><?= e($genWord((int)$level)) ?></span>
<div class="tree-inner">
<?php foreach ($levels as $li => $level): ?>
<?php $levelUnits = $units[$level] ?? []; ?>
<div class="tree-gen">
<div class="tree-gen-label">
<span class="gname"><?= e($genWord((int)$level)) ?></span>
<span class="gtag"><?= count($levelUnits) ?> 户</span>
</div>
<div class="tree-row" style="<?= e($colStyle) ?>">
<?php foreach ($levelUnits as $u): ?>
<div class="tree-unit">
<?php foreach ($u['ids'] as $k => $id): ?>
<?php if ($k === 1): ?>
<span class="tree-bond" aria-label="夫妻"><i></i></span>
<?php endif; ?>
<?php $n = $nodes[$id]; ?>
<a class="tnode <?= (int)$id === $meId ? 'is-me' : '' ?> <?= $n['ph'] ? 'is-placeholder' : '' ?> <?= $u['inlaw'] && $k === 0 ? 'is-inlaw' : '' ?>"
href="<?= e(url('people/show', ['id' => $n['id']])) ?>"
title="<?= e($n['name'] . ' · ' . $n['label']) ?>">
<span class="tph"><img src="<?= e(avatar_url($n['avatar'])) ?>" alt=""></span>
<span class="tname"><?= e($n['name']) ?></span>
<span class="tlabel"><?= e($n['label']) ?></span>
<?php if ($n['ph']): ?><span class="tph-mark">待确认</span><?php endif; ?>
</a>
<?php endforeach; ?>
<?php if ($u['inlaw']): ?>
<span class="tree-inlaw-tag" title="因婚姻进来的人,不是本家的血亲">姻亲</span>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
</div>
<div class="tree-row">
<?php foreach ($ids as $id): $n = $nodes[$id]; ?>
<a class="tnode <?= (int)$id === $meId ? 'is-me' : '' ?> <?= $n['ph'] ? 'is-placeholder' : '' ?>"
href="<?= e(url('people/show', ['id' => $n['id']])) ?>"
title="<?= e($n['name'] . ' · ' . $n['label']) ?>">
<span class="tph"><img src="<?= e(avatar_url($n['avatar'])) ?>" alt=""></span>
<span class="tname"><?= e($n['name']) ?></span>
<span class="tlabel"><?= e($n['label']) ?></span>
<?php if ($n['ph']): ?><span class="tph-mark">待确认</span><?php endif; ?>
</a>
<?php endforeach; ?>
</div>
</div>
<?php if ((int)$level !== (int)array_key_last($gens)): ?>
<div class="tree-link"></div>
<?php endif; ?>
<?php endforeach; ?>
<?php
// ---- 父母括号:这一代(父母)↔ 下一代(子女)的连线 ----
//
// 画法(每个「父母户 + 它的子女们」一组):
// · 一根横杆盖住「父母那一列 → 它的子女各列」;
// · 横杆在**父母列的中心**向上伸一根竖线,接到父母那一户;
// · 横杆在**每个子女列的中心**向下伸一根短线,接到各自的子女户。
// 于是「谁是谁的父母与孩子」是几何上可读的,而不是两代之间一条装饰线。
$childLevel = $levels[$li + 1] ?? null;
if ($childLevel !== null && !empty($units[$childLevel])):
$kids = $units[$childLevel];
$ranges = []; // 父母单元key => [第一个子女列, 最后一个子女列]
foreach ($kids as $i => $ku) {
$pk = $ku['parent_key'];
if ($pk === null) {
continue;
}
if (!isset($ranges[$pk])) {
$ranges[$pk] = [$i, $i];
} else {
$ranges[$pk][1] = $i;
}
}
// 父母单元key => 它在上一代里的列号(拿来决定竖线落点)
$pidx = [];
foreach (($units[$level] ?? []) as $i => $pu) {
$pidx[$pu['key']] = $i;
}
?>
<div class="tree-conn" style="<?= e($colStyle) ?>">
<?php foreach ($ranges as $pk => $rg): ?>
<?php
$pn = $unitName[$pk] ?? ['head' => '', 'spouse' => null];
$who = $pn['head'] . ($pn['spouse'] !== null ? ' 夫妇' : '');
$pCol = $pidx[$pk] ?? $rg[0];
$lo = min($pCol, $rg[0]);
$hi = max($pCol, $rg[1]);
$span = $hi - $lo + 1;
// 竖线落在父母列的中心(在横杆这一段里的百分比位置)
$pct = ($pCol - $lo + 0.5) / $span * 100;
?>
<div class="tc-bar" style="grid-column: <?= $lo + 1 ?> / <?= $hi + 2 ?>; grid-row: 1"
title="<?= e($who . ' 的子女') ?>">
<span class="tc-up" style="left: <?= round($pct, 3) ?>%"></span>
</div>
<?php endforeach; ?>
<?php foreach ($kids as $i => $ku): ?>
<div class="tc-down" style="grid-column: <?= $i + 1 ?>; grid-row: 1"></div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php endforeach; ?>
</div>
</div>
<p class="tree-legend">
「姻亲」= 因为婚姻进到这张图里的人(如我妻子的父母),不是本家的血亲。
括号横跨的那几户,就是括号上方那一户的子女。
</p>
<?php endif; ?>