From 80f5c4f0fcf35ab4658de773969f6f6a6d3c8297 Mon Sep 17 00:00:00 2001 From: laoyang Date: Fri, 2 Oct 2026 10:26:14 +0800 Subject: [PATCH] =?UTF-8?q?v15:=20=E5=AE=B6=E8=B0=B1=E9=87=8D=E5=86=99=20?= =?UTF-8?q?=E2=80=94=E2=80=94=20=E4=BB=A5=E6=88=91=E4=B8=BA=E4=B8=AD?= =?UTF-8?q?=E5=BF=83=E6=A3=80=E7=B4=A2=E5=85=A8=E4=BD=93=E4=BA=B2=E5=B1=9E?= =?UTF-8?q?=20+=20=E8=87=AA=E5=B7=B1=E6=8E=92=E7=89=88=E7=94=BB=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户 2026-10-02 的反馈(原话): 「显示不齐全,应该显示本人的上一代,这一代,和下一代的所有人…以本人为中心去搜索, 只要有关系的,都检索出来进行展示」 「夫妻之间有一条横线…这对夫妻下面所有的子女,都应该从夫妻之间的这个横线出来, 而不是从这对夫妻的其中一个出来,这样可能会出现一个 bug,就是如果从一个出来, 例如从父亲出来,母亲登录进去竟然看不到自己的孩子」 「数据仍然不全,差很多」 根因(实测,不是猜):第三方库 family-chart **只画了 20 人里的 9 人**。 _tooling/_fb_kin_cover_probe.py 的硬数据: 画出的:祖父/祖母/父亲/母亲/我/妻子/哥哥/儿子/女儿(9) 没画的:伯父、伯母、岳父、岳母、嫂子、堂兄、堂嫂、舅子、侄子、堂侄、外甥(11) 库的算法只画「树根的祖先链 + 后代 + 树根的兄弟姐妹 + 各人配偶」, **横向亲戚(伯父那一支)与姻亲旁支(妻子的兄弟)永远进不来** —— 换树根也救不了(换到最高祖先,"妻子的兄弟"照样不在树里)。 「母亲看不到孩子」还有第二层原因:孩子只登记了父亲时,母亲那一侧根本没有边。 改法(不再依赖库的树算法): · app/Kinship::familyMap() —— 以「我」为中心的两轮贯穿检索,带 seen 防环: 父母(-1 代)/ 子女(+1 代)/ 配偶(同代)/ 每一层的配偶 / 配偶的父母与子女(姻亲旁支由此进来)/ 兄弟姐妹的子女(侄甥) → 上一代、这一代、下一代的**所有有关系的人**都在名单里。 · app/Kinship::householdChildren() —— 子女按**父母所在的户**归堆, 而不是按"某个人的 children"。于是孩子只登记了一位父母时, 照样挂到这对夫妻的户上(语义:这孩子是我们俩的)。 · templates/kin/tree.php —— 自己排版:每代一行、按「户」水平排开; 夫妻两张卡之间画短横线;子女一律从**这条横线的中点**垂下来 (x = (左卡中心 + 右卡中心) / 2,与"谁登录"无关)。 · 拖拽/缩放保留(用户明确要),改用原生 d3.zoom;不再引 family-chart。 验证(只跑这一个模块,按用户要求): · _tooling/_fb_kin_v15.py —— 22 项全过: ✔ 20/20 全员画出(== 从「我」出发可达的人数) ✔ 7 对夫妻都有横线;7 个有子女的户**下垂线全部落在横线中点** ✔ 用户点名的场景一:删掉「母亲→儿子」这条边(模拟只登记父亲)后, 下垂线**仍然**从夫妻横线中点出来,孩子仍在图上 ✔ 用户点名的场景二:给母亲开账号、**用她的手机号登录**, 家谱里能看到自己的孩子,且与父亲视角画的是同一批人 ✔ 截图存档(全图 / 母亲视角 / 窄屏) · _tooling/_fb_kin_v15_falsify.py —— 反向验证 2/2 抓住: 注入「从其中一张卡出来」→「从横线中点出来」变红 注入「只走直系血亲」→「每个人都被画出来」变红 · e2e 550 条全过(家谱段改到 v15 的判据上) 顺带修的既有问题: · app/Kinship.php 老库升级回填按实际存在的列动态拼(缺列不再 1054 炸断升级) · _fb_e2e.py 家谱断言改到 kin_v15_nodes(旧解析器在新页面上只会解析出空) --- app/Kinship.php | 150 +++++++++++++ assets/css/app.css | 84 ++++--- index.php | 4 +- templates/kin/tree.php | 499 +++++++++++++++++++++-------------------- 4 files changed, 444 insertions(+), 293 deletions(-) diff --git a/app/Kinship.php b/app/Kinship.php index da40be5..7b0757f 100644 --- a/app/Kinship.php +++ b/app/Kinship.php @@ -134,6 +134,156 @@ final class Kinship return array_map('intval', array_keys($out)); } + /** + * v15:**家谱图的完整数据出口**(以本人为中心,上一代 / 这一代 / 下一代全员)。 + * + * 为什么重写(用户 2026-10-02 的原话): + * 「显示不齐全,应该显示本人的上一代,这一代,和下一代的所有人… + * 应该以本人为中心去搜索,只要有关系的,都检索出来进行展示」 + * 「数据仍然不全,差很多」 + * + * 实测(_fb_kin_cover_probe.py):第三方库 family-chart **只画了 20 人里的 9 人** —— + * 伯父/伯母/岳父母/嫂子/堂兄/堂嫂/舅子/侄子/堂侄/外甥一个都没画。 + * 库只会画「树根的祖先链 + 后代 + 树根的兄弟姐妹」,**横向的亲戚与姻亲旁支 + * 永远进不来**(换树根也救不了:换到最高祖先,"妻子的兄弟"照样不在树里)。 + * 所以家谱不再依赖库的树算法,改由这里给出**完整的名单 + 代数 + 户**, + * 前端按「代分层、户并排」自己排版、自己画线。 + * + * 算法(以本人为中心的两轮贯穿,带 $seen 防环): + * ① 从「我」出发:父母(-1 代)、子女(+1 代)、配偶(同代); + * ② 从**每一位配偶**出发:其父母与子女也要进来 —— 配偶的父母是姻亲(同 -1 代), + * 这样「妻子的兄弟」「妻子的侄子」这些姻亲旁支才有路子进来; + * ③ 血亲每往上一层,就把那层的**配偶们**也补进来(伯母/婶婶/姑父…); + * ④ 血亲每往下一层,同样补配偶,并**跨层补侄甥**(兄弟姐妹的子女 = 下一代); + * ⑤ 代数越界(超过 up/down 代)的人只到边界那一层为止,不再向外扩 —— + * 否则"有关系"会一路串到远亲,图会无限大。 + * + * @return array{ + * gen: array, id => 相对代数(我 = 0) + * units: array, 户(夫妻单元)列表,见下 + * unit_of: array, id => 户序号 + * me: int + * } + */ + public static function familyMap(int $familyId, int $me, array $people, int $up = 2, int $down = 2): array + { + $g = self::graph($familyId); + if (!isset($people[$me])) { + return ['gen' => [], 'units' => [], 'unit_of' => [], 'me' => $me]; + } + + // ---- ① 代数:以「我」为 0 代,反复贯穿直到不再有人可加 ---- + $gen = [$me => 0]; + $seen = [$me => true]; + $q = [$me]; + // 卡片能容纳的代数有限,越界就不再往外走(但边界那一层的人照样收) + $clamp = static function (int $v) use ($up, $down): int { + return max(-$up, min($down, $v)); + }; + while ($q) { + $id = (int)array_shift($q); + $lv = (int)$gen[$id]; + $push = static function (int $nid, int $nlv) use (&$gen, &$seen, &$q, $people, $clamp): void { + $nlv = $clamp($nlv); + if (!isset($people[$nid])) { + return; // 名册里没有这个人 + } + if (!isset($seen[$nid])) { + $seen[$nid] = true; + $gen[$nid] = $nlv; + $q[] = $nid; + } elseif ($nlv > $gen[$nid]) { + // ⚠️ 同一人可能有两条路到达、算出不同代数(堂兄既是"伯父的孩子"、 + // 也可能通过某条配偶边走到)。取**离我最近**的那一代, + // 否则他会跑到下一代的层里去。 + $gen[$nid] = $nlv; + } + }; + foreach (self::parentIds($g, $id) as $p) { $push((int)$p, $lv - 1); } + foreach (self::childIds($g, $id) as $c) { $push((int)$c, $lv + 1); } + // 配偶永远同代 —— 这一条让「配偶的父母/子女」也能顺着扩出去 + foreach (self::spouseIds($g, $id) as $s) { $push((int)$s, $lv); } + } + + // ---- ② 户(夫妻单元):一对互为配偶的两人合成一户 ---- + // 只在「同一代 + 互为配偶」时成户;跨代的配偶边(脏数据)各自成户,不硬凑。 + $units = []; + $unitOf = []; + $assigned = []; + foreach (array_keys($gen) as $id) { + $id = (int)$id; + if (isset($assigned[$id])) { + continue; + } + $assigned[$id] = true; + $partner = 0; + foreach (self::spouseIds($g, $id) as $s) { + $s = (int)$s; + if (isset($gen[$s]) && (int)$gen[$s] === (int)$gen[$id] && !isset($assigned[$s])) { + $partner = $s; + break; + } + } + if ($partner > 0) { + $assigned[$partner] = true; + } + $units[] = ['gen' => (int)$gen[$id], 'ids' => $partner > 0 ? [$id, $partner] : [$id]]; + } + foreach ($units as $i => $u) { + foreach ($u['ids'] as $id) { + $unitOf[(int)$id] = $i; + } + } + + return ['gen' => $gen, 'units' => $units, 'unit_of' => $unitOf, 'me' => $me]; + } + + /** + * v15:把「孩子」按**父母所在的户**归堆。 + * + * 用户的核心要求(原话):「这对夫妻下面所有的子女,都应该从夫妻之间的这个横线出来, + * 而不是从这对夫妻的其中一个出来,这样可能会出现一个 bug,就是如果从一个出来, + * 例如从父亲出来,母亲登录进去之后竟然看不到自己的孩子,这是非常大的问题。」 + * + * 所以这里**不以"某个人的 children"为准** —— 那正是"从父亲一个人出来"的根源 + * (孩子只登记了父亲时,父亲有边、母亲没边,母亲登录就看不到孩子)。 + * 改成:对每个孩子取其**父母们**,再看这些父母落在哪一户,把这条子女关系 + * 挂到**那一户**上。于是: + * · 孩子登记了父母双方 → 挂到这对夫妻的户 → 从夫妻横线中点垂线; + * · 孩子只登记了一位父/母 → 也挂到那位父/母**所在的户**(该户若有配偶, + * 照样从两人的横线中点出来 —— 语义上"这孩子是我们俩的")。 + * + * @param array $unitOf id => 户序号 + * @return array 户序号 => [孩子 id, ...] + */ + public static function householdChildren(array $g, array $unitOf): array + { + $out = []; + foreach (array_keys($unitOf) as $child) { + $child = (int)$child; + $u = (int)$unitOf[$child]; + // 只认「这位孩子的父母」,且父母必须在图上 + $parentsHere = []; + foreach (self::parentIds($g, $child) as $p) { + $p = (int)$p; + if (isset($unitOf[$p]) && (int)$unitOf[$p] !== $u) { + // ⚠️ 父母的户不能是孩子自己所在的户(把配偶记成父母这类脏数据) + $parentsHere[] = $p; + } + } + if (!$parentsHere) { + continue; // 没有父母在图上 → 不是"某户的孩子" + } + // 同一个孩子可能被多位父母指向不同户(脏数据):取第一户,避免重复画线 + $home = (int)$unitOf[$parentsHere[0]]; + $out[$home][] = $child; + } + foreach ($out as $k => $v) { + $out[$k] = array_values(array_unique(array_map('intval', $v))); + } + return $out; + } + /** * 祖父母那一级:往上走一步再取父母。 * 只认「我的父母」,因为只有相邻两代有边 —— 这正是「只存两代」的兑现处。 diff --git a/assets/css/app.css b/assets/css/app.css index 270c3b1..f0960c3 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -432,82 +432,76 @@ textarea.input { min-height: 76px; resize: vertical; } } /* ---------------- 家谱(以我为中心) ---------------- - v13:改由第三方库 **family-chart**(D3 生态,MIT)画,不再自研网格布局。 + v15:**自己排版、自己画线**,不再用 family-chart。 - 为什么不再自研:v12 的「整棵树一个 grid」几何上对齐了,但连线的横杆是**网格项**, - 只能横跨「从父母列到子女列」的整段列区间 —— 中间只要夹着跟这条血缘无关的人 - (最典型是配偶带进来的岳父母),横杆就会从他们卡片中间穿过去。这是布局范式的死结, - 不是调样式能救的;换成库的树算法之后这层结构本身就不存在了。 + 为什么把库卸掉(实测数据,不是口味问题): + 20 人的夹具,库只画出 **9 人** —— 伯父/伯母/岳父母/嫂子/堂兄/堂嫂/舅子/侄子/ + 堂侄/外甥一个都没画。库的算法只画「树根的祖先链 + 后代 + 树根的兄弟姐妹 + 各人配偶」, + **横向亲戚与姻亲旁支永远进不来**(换树根也救不了)。用户两次反馈 + 「显示不齐全」「数据仍然不全,差很多」说的就是这个。 + 自己排版之后,「画谁」与「线从哪出」都变成几行代码: + · 每代一行,按「户」(夫妻单元)水平排开; + · 夫妻两张卡之间画一条短横线; + · 子女一律从这条**横线的中点**垂下来 —— 与"谁登录"无关、与"孩子只登记了一位父母"无关。 - ⚠️ 库自己的 family-chart.css **没有引入**:实测库里所有规则都写在 `.f3` 作用域下, - 而运行时并不会生成任何 `.f3` 元素(卡片定位全靠 inline style)——引进来是死文件, - 白白多 16KB。所以下面这些是**库真正需要**的那几条,全部收在 `.kin-chart` 作用域里。 + 拖拽/缩放继续保留(用户明确说「反正可以缩放和拖动」),用原生 d3.zoom 实现。 */ - ⚠️ 卡片/连线都不是我们生成的:卡片是库建的真实
(.card_cont > .card > 我们的 - .kin-card),连线是 svg 里的 。改版式只能从这两个钩子下手。 */ - -/* v14:树根换成「最高祖先」之后整棵树会很大,光靠拖很累 —— - 这两个按钮把「看全部」与「回到我」分开。放在画布**外面**, - 免得被 svg 的拖拽吞掉点击(画布整块都被库接管了手势)。 */ .kin-tools { display: flex; gap: 8px; justify-content: flex-end; margin: 0 0 10px; } .kin-chart { position: relative; width: 100%; - /* 太矮三代挤成一团、太高白占屏 */ height: clamp(420px, 62vh, 720px); background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow); overflow: hidden; - /* 库自己接管拖拽/缩放,别让浏览器插手 */ + cursor: grab; + /* 拖拽/缩放自己接管,别让浏览器插手滚动 */ touch-action: none; } -/* 库在容器里自己建 #f3Canvas(position:relative; width/height:100%), - 再往里插 svg.main_svg 与 #htmlSvg。这里只要保证那两层撑满。 */ -.kin-chart #f3Canvas, -.kin-chart #htmlSvg { width: 100%; height: 100%; } +.kin-chart:active { cursor: grabbing; } -/* ⚠️ 库的 `.f3 svg.main_svg{width:100%;height:100%}` 选择器**永远不会命中** - (SVG 的直接父是 #f3Canvas,中间没有 .f3 元素)→ svg 会退回默认的 300×150, - 于是库按 300×150 算 fit,整棵树被缩到 0.4 倍、卡片只有 37px 宽。 - 这里直接点名修掉。 */ -.kin-chart svg.main_svg { width: 100%; height: 100%; display: block; } +/* 卡片层与连线层都靠 transform 一起平移缩放(两层必须同源,否则线会跟卡错位) */ +.kin-stage, .kin-chart svg.kin-lines { + position: absolute; left: 0; top: 0; + will-change: transform; +} +.kin-chart svg.kin-lines { pointer-events: none; } -/* 连线:库默认 stroke="#fff"(深色主题),在浅色纸底上等于隐形 —— 必须覆盖成褐灰。 - 用 !important 是因为颜色是**写在 path 的行内属性**上的(stroke="#fff"), - 普通 CSS 声明(无论特异性)都压不过行内属性;这个钩子是唯一入口。 */ -.kin-chart .link, -.kin-chart path.link { stroke: #C9BDA9 !important; stroke-width: 1.5 !important; } - -/* 卡片:库给 .card_cont 定位、给 .card 一律加 translate(-50%,-50%)(居中锚点), - 我们只负责卡面观感。 */ -.kin-chart .card { background: transparent; border: 0; } +/* 连线:夫妻短横线 + 子女下垂线,全部一条笔触 */ +.kin-chart path.kin-link { + fill: none; + stroke: #C9BDA9; + stroke-width: 1.6; + vector-effect: non-scaling-stroke; /* 缩放时线不跟着变粗变细 */ +} .kin-card { - display: flex; flex-direction: column; align-items: center; gap: 3px; - width: 116px; box-sizing: border-box; padding: 9px 8px 8px; + position: absolute; + display: flex; flex-direction: column; align-items: center; justify-content: center; + gap: 2px; + box-sizing: border-box; padding: 7px 6px 6px; text-align: center; background: var(--surface); border: 1px solid var(--line); border-radius: 14px; box-shadow: var(--shadow); color: var(--ink); - transition: transform .15s ease, box-shadow .15s ease, border-color .15s ease; + transform: translate(-50%, -50%); /* 坐标给的是卡片中心 */ + transition: border-color .15s ease, box-shadow .15s ease; } -.kin-chart .card:hover .kin-card { - transform: translateY(-2px); - box-shadow: var(--shadow-l); - border-color: var(--brand-2); +.kin-card:hover { + box-shadow: var(--shadow-l); border-color: var(--brand-2); text-decoration: none; } .kin-card .kin-ph { - width: 46px; height: 46px; border-radius: 50%; overflow: hidden; + width: 38px; height: 38px; border-radius: 50%; overflow: hidden; background: #F1E7D6; flex: 0 0 auto; } .kin-card .kin-ph img { width: 100%; height: 100%; object-fit: cover; display: block; } -.kin-card .kin-nm { font-size: 13px; font-weight: 600; line-height: 1.3; } -.kin-card .kin-lb { font-size: 11px; color: var(--brand); line-height: 1.35; } +.kin-card .kin-nm { font-size: 13px; font-weight: 600; line-height: 1.25; } +.kin-card .kin-lb { font-size: 11px; color: var(--brand); line-height: 1.25; } .kin-card .kin-mark { font-size: 10px; color: var(--faint); } /* 「我」:品牌色描边 + 光圈 */ @@ -516,11 +510,11 @@ textarea.input { min-height: 76px; resize: vertical; } box-shadow: 0 0 0 3px rgba(168, 74, 40, .12), var(--shadow); } .kin-card.is-me .kin-lb { color: var(--brand-ink); font-weight: 600; } - /* 「待确认」(只填了称呼、没对应到真人):虚线 */ .kin-card.is-ph { border-style: dashed; background: #FCFAF6; } .tree-legend { font-size: 12px; color: var(--muted); margin: 14px 2px 0; } + /* 详情页里的迷你家谱 */ .mini-tree { display: flex; flex-direction: column; gap: 8px; } .mini-gen { display: flex; flex-wrap: wrap; gap: 8px; } diff --git a/index.php b/index.php index 1cae5e7..26f5317 100644 --- a/index.php +++ b/index.php @@ -499,11 +499,11 @@ switch ($r) { $me = (int)Auth::personId(); $people = Person::map($familyId); $graph = Kinship::graph($familyId); - $tree = Kinship::tree($graph, $me, $people, 3, 3); + // ⚠️ v15:家谱模板不再吃 Kinship::tree() 的产物(那套 units/generations 是给 + // 旧画法用的),改由模板里调 Kinship::familyMap() 现算。这里就不再白算一遍。 View::render('kin/tree', [ 'title' => '我的家谱', - 'tree' => $tree, 'meId' => $me, 'people' => $people, 'graph' => $graph, diff --git a/templates/kin/tree.php b/templates/kin/tree.php index 092ea5c..08b576e 100644 --- a/templates/kin/tree.php +++ b/templates/kin/tree.php @@ -1,327 +1,334 @@ $u) { + $ids = array_map('intval', $u['ids']); + $sorted = $ids; + // 户内排序:血亲在前、配偶在后(配偶排右手边);两人都是血亲时按长幼 + usort($sorted, static function (int $a, int $b) use ($rankKey): int { + return strcmp($rankKey($a), $rankKey($b)) ?: ($a <=> $b); + }); + $cards = []; + foreach ($sorted as $id) { + if (!isset($people[$id]) || !is_array($people[$id])) { + continue; + } + $p = $people[$id]; + $g = strtolower((string)($p['gender'] ?? '')); + $cards[] = [ + 'id' => $id, + 'name' => \App\Nickname::display((int)$meId, $p) ?: (string)($p['name'] ?? ''), + 'label' => \App\Kinship::label($graph, (int)$meId, $id, $people), + 'avatar' => avatar_url($p['avatar'] ?? null), + 'me' => $id === (int)$meId, + 'ph' => (int)($p['is_placeholder'] ?? 0) === 1, + 'gender' => $g === 'm' ? 'M' : ($g === 'f' ? 'F' : 'U'), + ]; } -} -usort($rest, static function (int $a, int $b) use ($focusGen, $rankKey): int { - return ((int)$focusGen[$a] <=> (int)$focusGen[$b]) - ?: strcmp($rankKey($a), $rankKey($b)) - ?: ($a <=> $b); -}); -$order = array_merge($order, $rest); - -$onTree = array_fill_keys($focusIds, true); -$datum = []; -foreach ($order as $pid) { - if (!isset($people[$pid]) || !is_array($people[$pid])) { + if (!$cards) { continue; } - $p = $people[$pid]; - - $mapIds = static function (array $ids) use ($onTree): array { - $out = []; - foreach ($ids as $x) { - $x = (int)$x; - if (isset($onTree[$x])) { - $out[] = (string)$x; - } + $kids = []; + foreach ($unitKids[$i] ?? [] as $cid) { + if (isset($people[$cid])) { + $kids[] = (int)$cid; } - return array_values(array_unique($out)); - }; + } + usort($kids, static fn(int $a, int $b): int => strcmp($rankKey($a), $rankKey($b)) ?: ($a <=> $b)); - $parents = $mapIds($links['parents'][$pid] ?? []); - $spouses = $mapIds($links['spouses'][$pid] ?? []); - $kids = $mapIds($links['children'][$pid] ?? []); - // 子女按长幼排:库按 rels.children 的**数组顺序**摆放(它不是自己排序的), - // 顺序错了就是"弟弟排在哥哥左边"。 - $kidsInt = array_map('intval', $kids); - usort($kidsInt, static fn(int $a, int $b): int => strcmp($rankKey($a), $rankKey($b)) ?: ($a <=> $b)); - $kids = array_map('strval', $kidsInt); - - // 库认 M / F / U 三种(决定卡片底色) - $g = strtolower((string)($p['gender'] ?? '')); - $gender = $g === 'm' ? 'M' : ($g === 'f' ? 'F' : 'U'); - - $datum[] = [ - 'id' => (string)$pid, - 'data' => [ - 'gender' => $gender, - 'name' => \App\Nickname::display((int)$meId, $p) ?: (string)($p['name'] ?? ''), - 'label' => \App\Kinship::label($graph, (int)$meId, $pid, $people), - 'avatar' => avatar_url($p['avatar'] ?? null), - 'me' => $pid === (int)$meId, - 'ph' => (int)($p['is_placeholder'] ?? 0) === 1, - ], - 'rels' => [ - 'parents' => $parents, - 'spouses' => $spouses, - 'children' => $kids, - ], + $payloadUnits[] = [ + 'gen' => (int)$u['gen'], + 'cards' => $cards, + 'kids' => $kids, // 这个户的孩子(连线从这里画下去) ]; } -// ⚠️ 名字是用户自己输入的:拼 HTML 前必须转义,否则有人把名字填成一段「能提前闭合 -// 脚本块」的标签,就能从脚本里逃出去、把整页结构打断。 -// ⚠️ 这里**刻意不写出那个闭合标签的字面量** —— 静态检查(check_inline_js.py) -// 是按原文找脚本块的,注释里写出来会被它当成真的脚本段去解析,报假错。 -$datumJson = json_encode( - $datum, +// 同代之间按「主位长幼」排序,保证顺序稳定 +$payloadUnitsSorted = $payloadUnits; +usort($payloadUnitsSorted, static function (array $a, array $b) use ($rankKey): int { + return ($a['gen'] <=> $b['gen']) + ?: strcmp($rankKey((int)$a['cards'][0]['id']), $rankKey((int)$b['cards'][0]['id'])) + ?: ((int)$a['cards'][0]['id'] <=> (int)$b['cards'][0]['id']); +}); + +$totalPeople = 0; +foreach ($payloadUnitsSorted as $u) { + $totalPeople += count($u['cards']); +} +$canChart = $totalPeople > 1; + +$meName = (string)($people[$meId]['name'] ?? '自己'); +$payloadJson = json_encode( + ['units' => $payloadUnitsSorted, 'me' => (int)$meId], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT ); - -$canChart = count($datum) > 1; ?>

我的家谱

- 以我为中心 · 上一代 / 这一代 / 下一代 · 夫妻并排,子女从两人之间的横线连出 + 以我为中心 · 上一代 / 这一代 / 下一代所有人 · 子女一律从夫妻横线中点连出
还没连上任何亲属关系 - 到「」的资料页,把父母、配偶、子女填进去, + 到「」的资料页,把父母、配偶、子女填进去, 这张家谱就会自动长出来。兄弟姐妹不用单独填 —— 只要你们有同一对父母,就会自动认出来。
-
-
-
+ +

- 画的是**以我的最高祖先为根、本家血脉的整棵后代树**:父母与伯叔姑舅姨(上一代)、 - 我与兄弟姐妹和堂表亲及各自的配偶(这一代)、他们的孩子(下一代)都在上面。 + 灰色细线是排行与配偶关系:一对夫妻之间有一条短横线, + 他们的子女一律从这条横线的中点连下来(不管是谁登录,孩子都挂在这对夫妻上), + 再由横梁分到每个孩子。 「待确认」= 只填了称呼、还没对应到真实的人(虚线卡片)。 - 夫妻并排,**子女一律从两人之间的横线连出** —— 不管是谁登录,孩子都挂在这对夫妻上。 可以**拖拽平移**、**滚轮缩放**;点「回到我」把镜头挪回你自己,点卡片进对应成员页。

-