Files
FamilyBook/templates/kin/tree.php
T
laoyang 4060c18f86 fix(kin,movies): 家谱连线重构为单一网格 + 观影设置统一页头
需求三点:
1. 观影模块「⚙ 设置」与「记一场电影」按钮高度不一致 → 统一页头动作区
   四个选择器的 padding/font-size(.btn 与 .btn-sm 混用是根因);并把设置
   下拉推广到观影全部页面(详情/影院/搜索/统计/报告/表单),落在返回按钮旁。
2. 家谱连线「线乱」→ 根因三条:每代各建一个网格(列宽最小值由内容决定,
   各代不一致,窄屏实测 93.5px vs 164/78/78/78px);落点用百分比算而 grid
   间距不参与百分比;连线行与卡片行不是同一套列。
   改法:整棵树只留一个网格,辈分行/卡片行/连线行全是它的行,连线元素本身
   也是网格项(靠 justify-self 落位);列宽固定为一卡之宽(不用 minmax/1fr,
   否则宽屏卡片中心 ≠ 列中心)。
3. 亲属关系接口改白名单(parent/child/spouse)——原先未知 kind 被静默记成
   配偶,夹具传 kind=sibling 让家谱断言长期拿错数据自证。

设置下拉抽成可复用片段 _settings_menu.php($setCurrent 高亮当前页、
$setCanWrite 控「影院管理」显隐),六七个页头不再各抄一份。

验证:e2e 456/0、桌面几何 268/0、移动端几何 504/0、交互 97/0、
家谱连线诊断 0 失败、静态空跑 exit=0、静态断言证伪 13/13、
反向验证 68/68 全抓住。
2026-10-01 17:42:15 +08:00

184 lines
9.7 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
/**
* 我的家谱:以我为中心,向上向下各三代。**v12 重写** —— 从「每代各一个网格」改成「全树一个网格」。
*
* 为什么必须重写(v11 实测到的三个真缺陷):
* ① **每代各建一个网格 → 各代的列宽可以不一样**。`repeat(N, minmax(min-content,1fr))`
* 的最小值是**内容**决定的:有夫妻的那一代最小要「两卡 + 短横」,单身那代只要一卡。
* 窄屏(430px)实测:父母辈列宽 93.5px、我这一辈 164/78/78/78px —— 列坐标根本不一样,
* 同一条竖线在上下两代相差 31~62px,线自然全乱。
* ② 横杆上的竖线落点用**百分比**算,而 grid 的列间距不参与百分比 —— 列一多就系统性偏移。
* ③ 连线行按「代」切分,与所属的网格**不是同一套列**,无法从几何上保证对齐。
*
* v12 的做法:**整棵树只用一个 grid**(`$colStyle` 挂在 .tree-grid 上),
* 「辈分行 / 卡片行 / 连线行」只是这张网格里的不同行:
* · 卡片行:每户占 grid-column 的一段(夫妻占 2 列、单人占 1 列);
* · 连线行:横杆、上引线、下引线**都是网格项**,各自占自己的列并 `justify-self:center`,
* 于是「竖线落在哪」是浏览器按列算的 —— 不需要任何 px/百分比换算,天然对齐。
* 各代的列因此完全一致,横向滚动仍交给 .tree-inner。
*
* 称呼、姻亲、长幼排序这些语义一个字没改,仍由 App\Kinship::tree() / label() 决定。
*/
$gens = $tree['generations'];
$units = $tree['units'] ?? [];
$unitName = $tree['unit_names'] ?? [];
$nodes = $tree['nodes'];
$me = $nodes[$meId] ?? null;
// 代数 → 人类说法
$genWord = function (int $level): string {
if ($level === 0) {
return '我这一辈'; // ⚠️ 必须先判 0:它既不 <0 也不在 $down 里,
// 漏判就会显示成「下 0 代」这种没人看得懂的东西
}
if ($level < 0) {
$up = [-3 => '曾祖辈', -2 => '祖辈', -1 => '父母辈'];
return $up[$level] ?? '上 ' . abs($level) . ' 代';
}
$down = [1 => '子女辈', 2 => '孙辈', 3 => '曾孙辈'];
return $down[$level] ?? '下 ' . $level . ' 代';
};
$levels = array_keys($gens);
// ---- 把每一代摊成「列位(slot)」 ----
// 一「户」占几个列位:夫妻 2(并排 + 中间一根短横)、单人 1。
// 整棵树的列数取最宽那一代;**所有代共用这一套列**,竖线才可能对齐。
$slotOf = []; // level => [单元key => ['start'=>起始列位, 'span'=>占几列]]
$maxSlots = 1;
foreach ($levels as $level) {
$s = 0;
foreach (($units[$level] ?? []) as $u) {
$span = $u['spouse'] !== null ? 2 : 1;
$slotOf[$level][$u['key']] = ['start' => $s, 'span' => $span];
$s += $span;
}
// ⚠️ 至少要 1:整棵树只有「我」一个人时也不能写出 repeat(0, …)(那是无效值)
$maxSlots = max($maxSlots, max(1, $s));
}
// 列宽**固定 = 一卡之宽**(--col-w:桌面 96 / 窄屏 78),不是 1fr。
//
// ⚠️ 为什么必须是固定值而不是 minmax(96px,1fr):列宽给 1fr 时,宽屏下列被撑到 ~195px,
// 而卡片只有 96px、在户内 flex 居中 —— 于是**卡片中心 ≠ 列中心**。
// 连线靠 justify-self:center 落在**列**中心,就会与卡片错开(实测夫妻户偏 50px)。
// 列宽固定成 96px 后:
// · 单列户 宽 = 96 = 卡片宽 → 卡片正好居中,中心 = 列中心;
// · 夫妻户 宽 = 96+14+96 = 两卡 + 一根短横 → 内容正好填满,两卡各自落在自己的列上。
// 两列的间距(gap)与夫妻短横同宽,所以「户内的两个卡」与「跨户的两个卡」间距也一致。
// 木桶效应不存在了:列宽恒定 → 卡片永远不会被压到地板以下(重叠问题从构造上消失)。
$colStyle = 'grid-template-columns: repeat(' . (int)$maxSlots . ', var(--col-w));';
// 网格行号:每代占 3 行 —— 「辈分行 / 卡片行 / 连线行」。
// 行号写死(而不是靠自动排布)是为了让上引线、横杆、下引线精确落到同一行。
$rowLabel = function (int $li): int { return $li * 3 + 1; };
$rowCards = function (int $li): int { return $li * 3 + 2; };
$rowConn = function (int $li): int { return $li * 3 + 3; };
?>
<div class="page-head">
<h1>我的家谱</h1>
<span class="sub">同代按长幼从左到右 · 夫妻并排 · 折线表示谁是谁的父母</span>
</div>
<?php if (!$nodes || count($nodes) <= 1): ?>
<div class="empty">
<b>还没连上任何亲属关系</b>
到「<?= e($me['name'] ?? '自己') ?>」的资料页,把父母、配偶、子女填进去,
这张家谱就会自动长出来。兄弟姐妹不用单独填 —— 只要你们有同一对父母,就会自动认出来。
</div>
<?php else: ?>
<div class="tree">
<div class="tree-inner">
<div class="tree-grid" style="<?= e($colStyle) ?>">
<?php foreach ($levels as $li => $level): ?>
<?php $levelUnits = $units[$level] ?? []; ?>
<div class="tree-gen-label" style="grid-column: 1 / -1; grid-row: <?= $rowLabel($li) ?>">
<span class="gname"><?= e($genWord((int)$level)) ?></span>
<span class="gtag"><?= count($levelUnits) ?> 户</span>
</div>
<?php foreach ($levelUnits as $u): ?>
<?php
$sl = $slotOf[$level][$u['key']];
$unitStyle = 'grid-column: ' . ($sl['start'] + 1) . ' / span ' . $sl['span']
. '; grid-row: ' . $rowCards($li) . ';';
?>
<div class="tree-unit" style="<?= e($unitStyle) ?>">
<?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; ?>
<?php
// ---- 连线行:这一代(父母)↔ 下一代(子女)----
//
// 每个「父母户 + 它的子女们」画一架**折线**(family elbow):
// · 横杆盖住「父母列 → 它的子女各列」;
// · 上引线从横杆中点往上接到父母那一户(落在父母**整户的中心列**上);
// · 每个子女各一根下引线,接进同一根横杆(落在**血亲本人那一列**的中心)。
// 三样都是网格项、都靠 justify-self:center 定位 —— 落点由浏览器按列算,必然对齐。
$childLevel = $levels[$li + 1] ?? null;
if ($childLevel !== null && !empty($units[$childLevel])):
// 父母户key => 它的子女们的列位起点
$kidStarts = [];
foreach ($units[$childLevel] as $ku) {
$pk = $ku['parent_key'];
if ($pk !== null && isset($slotOf[$level][$pk])) {
$kidStarts[$pk][] = $slotOf[$childLevel][$ku['key']]['start'];
}
}
$bars = [];
$downs = [];
foreach ($kidStarts as $pk => $starts) {
$ps = $slotOf[$level][$pk];
$lo = min($ps['start'], min($starts));
$hi = max($ps['start'] + $ps['span'] - 1, max($starts));
$bars[] = ['lo' => $lo, 'hi' => $hi, 'pStart' => $ps['start'], 'pSpan' => $ps['span'],
'who' => ($unitName[$pk]['head'] ?? '') . (($unitName[$pk]['spouse'] ?? null) !== null ? ' 夫妇' : '')];
foreach ($starts as $cs) {
$downs[] = $cs;
}
}
sort($downs);
?>
<?php foreach ($bars as $b): ?>
<?php $span = $b['hi'] - $b['lo'] + 1; ?>
<div class="tc-bar" style="grid-column: <?= $b['lo'] + 1 ?> / span <?= $span ?>; grid-row: <?= $rowConn($li) ?>"
title="<?= e($b['who'] . ' 的子女') ?>"></div>
<div class="tc-up" title="<?= e($b['who']) ?>"
style="grid-column: <?= $b['pStart'] + 1 ?> / span <?= $b['pSpan'] ?>; grid-row: <?= $rowConn($li) ?>"></div>
<?php endforeach; ?>
<?php foreach ($downs as $cs): ?>
<div class="tc-down" style="grid-column: <?= $cs + 1 ?>; grid-row: <?= $rowConn($li) ?>"></div>
<?php endforeach; ?>
<?php endif; ?>
<?php endforeach; ?>
</div>
</div>
</div>
<p class="tree-legend">
「姻亲」= 因为婚姻进到这张图里的人(如我妻子的父母),不是本家的血亲。
折线横跨的那几户,就是它上方那一户的子女;连线从**血亲本人**头上接出。
</p>
<?php endif; ?>