From c1a97c7d81233f70ca123747bf8265775d2800d4 Mon Sep 17 00:00:00 2001 From: laoyang Date: Fri, 2 Oct 2026 19:24:08 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=B6=E8=B0=B1=20v21=EF=BC=9A=E6=94=B9?= =?UTF-8?q?=E5=9B=9E=20family-chart=20=E5=BA=93=E6=B8=B2=E6=9F=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户明确要求「还是用 family-chart 来实现吧,刚才查了这个库可以完美实现 我想要的效果,之前一定是你调用的方法不对」。自研区间法(v20)虽然 20/20 全显示且通过全部版面检查,但用户觉得不够顺滑细腻,改为交给成熟库。 == 关键修复(都是真跑出来的坑)== 1. **IIFE 首行就 return**:本地变量 `var f3 = root.querySelector(...)` 与 全局库 `f3` 重名,`typeof f3 === 'undefined'` 因变量提升恒为真 → 整段 JS 一行没执行、画布永远空白。改名 `f3Canvas`。 2. **「回到我」不动卡片层**:库的 zoom 行为绑在 `#f3Canvas` 上,不是 `svg.main_svg`。拿 svg 的 `__zoomObj` 去驱动只会动 SVG 连线层, HTML 卡片层(`#htmlSvg .cards_view`)留在原地。改取 `#f3Canvas`。 3. **SVG 退化成 300×150**:库自带 CSS 写的是 `.f3 svg.main_svg{width:100%; height:100%}`,而我们的容器类名是 `.kin-chart`(不是 `.f3`)、也没引 库的 CSS → SVG 根元素用浏览器默认尺寸,所有画到框外的连线被裁掉。 症状很隐蔽:卡片层是 HTML div 不受影响,于是表现为「卡在、线没了」。 补 `.kin-chart svg.main_svg{width:100%;height:100%}`。 4. **儿子/女儿这一辈不显示**:树根是最高祖先,`progeny_depth` 必须覆盖到 「我」的下一代 → 从 2 调到 3。 5. **连线是白的看不见**:库给 path 写了 inline `stroke="#fff"`,普通 CSS 覆盖不了,必须 `!important`。 == 免费版能力边界(已实测确认,非调用问题)== 库的算法只画「树根的祖先链 + 后代 + 树根的兄弟姐妹 + 各人配偶」, **不会从配偶继续向上/向下 walk**。20 人夹具里岳父/岳母/舅子/外甥 (4 位姻亲)画不出来(换树根、调 depth 都救不了)→ 图上 16/20。 服务端 data-datum 仍含全体(含姻亲),名册/称呼不受影响。 == 验证战绩 == - e2e 549 通过 / 0 失败 - 桌面几何 368 / 0 - 移动端几何 516 / 0(家谱断言改到 family-chart 选择器) - 交互 97 / 0;升级专测 123 / 0 - 家谱可视化 24 / 0(含「svg 不是 300×150」这条,正是坑 3 的捕手) - 家谱版面 10 / 0 - 反向验证 3 / 3(间距压扁→叠卡;depth=1→缺人;写坏 centerOn 平移量→偏离中心) --- assets/css/app.css | 83 +++-- templates/kin/tree.php | 726 +++++++++++------------------------------ 2 files changed, 230 insertions(+), 579 deletions(-) diff --git a/assets/css/app.css b/assets/css/app.css index 04839ae..dbffd88 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -432,19 +432,14 @@ textarea.input { min-height: 76px; resize: vertical; } } /* ---------------- 家谱(以我为中心) ---------------- - v15:**自己排版、自己画线**,不再用 family-chart。 + v21:改用 family-chart 库渲染。库把 data[0].id 当 main_id, + 所以把「最高祖先」排第一位以画出尽可能多的血亲旁支; + 加载后再把镜头平移回「我」,兼顾"看到全树"与"以自己为中心"。 - 为什么把库卸掉(实测数据,不是口味问题): - 20 人的夹具,库只画出 **9 人** —— 伯父/伯母/岳父母/嫂子/堂兄/堂嫂/舅子/侄子/ - 堂侄/外甥一个都没画。库的算法只画「树根的祖先链 + 后代 + 树根的兄弟姐妹 + 各人配偶」, - **横向亲戚与姻亲旁支永远进不来**(换树根也救不了)。用户两次反馈 - 「显示不齐全」「数据仍然不全,差很多」说的就是这个。 - 自己排版之后,「画谁」与「线从哪出」都变成几行代码: - · 每代一行,按「户」(夫妻单元)水平排开; - · 夫妻两张卡之间画一条短横线; - · 子女一律从这条**横线的中点**垂下来 —— 与"谁登录"无关、与"孩子只登记了一位父母"无关。 - - 拖拽/缩放继续保留(用户明确说「反正可以缩放和拖动」),用原生 d3.zoom 实现。 */ + 免费版 family-chart 的算法只画「树根的祖先链 + 后代 + 树根的兄弟姐妹 + 各人配偶」, + **不会从配偶继续向上/向下 walk**,因此岳父岳母、舅子、外甥等姻亲旁支 + 在单棵树里天然画不出来(换树根、调 depth 都救不了)。 + 这是库的能力边界,不是调用方式问题;如要姻亲,需要 Premium 或多树拼接。 */ .kin-tools { display: flex; gap: 8px; justify-content: flex-end; margin: 0 0 10px; } @@ -458,37 +453,46 @@ textarea.input { min-height: 76px; resize: vertical; } box-shadow: var(--shadow); overflow: hidden; cursor: grab; - /* 拖拽/缩放自己接管,别让浏览器插手滚动 */ + /* 拖拽/缩放由库接管,别让浏览器插手滚动 */ touch-action: none; } .kin-chart:active { cursor: grabbing; } -/* 卡片层与连线层都靠 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; z-index: 1; } -/* ⚠️ v18:参考实现(亲友+)把连线画在**卡片背后**(ViewGroup.onDraw 在子 View 之前), - 所以线可以从卡中心连到卡中心、被卡面挡住即可 —— 不需要绕路。 - 这里用 z-index 保证「连线在卡片下面」。 */ -.kin-stage { z-index: 2; } +/* 隐藏 family-chart 自带的空导航栏(我们用自己的「看全部 / 回到我」按钮) */ +.kin-chart .f3-nav-cont { display: none !important; } -/* 连线:夫妻短横线 + 子女下垂线,全部一条笔触 */ -.kin-chart path.kin-link { - fill: none; - stroke: #C9BDA9; - stroke-width: 1.6; - vector-effect: non-scaling-stroke; /* 缩放时线不跟着变粗变细 */ -} +/* ⚠️ 这条是**必须**的:库自带的 CSS 里写着 `.f3 svg.main_svg { width:100%; height:100% }`, + 而我们的容器类名是 `.kin-chart`(不是 `.f3`),也没引库的 CSS —— + 于是 SVG 根元素退化成浏览器默认的 **300×150**,所有画到框外的连线被裁掉 + (卡片层是 HTML div,不受影响,所以"卡在、线没了"这个症状很隐蔽)。 + 实测:不写这条时 svg 只有 300×150(`_fb_kin_check_v17.py` 第②条会红)。 */ +.kin-chart svg.main_svg { width: 100%; height: 100%; display: block; z-index: 1; } -/* v18:卡片改成「上图下名 + 称呼」的窄长形,对齐参考实现(亲友+)的比例: - 参考里 50dp 宽 × 80dp 高,头像占满上半、姓名与称呼各一行贴在下半。 - 我们把 88×106 按同样比例放大 —— 窄一点才排得下"好几代 + 兄弟姐妹"。 */ +/* family-chart 的 HTML 卡片层 #htmlSvg 与 SVG 连线层由库的 onZoom 同步 transform。 + 卡片容器 .card_cont 已经 absolute + translate(x,y);里面再放我们的 .kin-card。 */ +.kin-chart #htmlSvg { z-index: 2; } +.kin-chart .card_cont { pointer-events: none; } + +/* 库给卡片套了一层
/
, + 我们只接管最里面那层(.kin-card)。把外层的中转容器归零,避免它自带的内边距/背景 + 让卡片看起来"胖了一圈"或有底色。 */ +.kin-chart .card_cont > .card, +.kin-chart .card_cont .card-inner { + background: transparent; + padding: 0; + border-radius: 0; + min-height: 0; + width: auto; + height: auto; +} +.kin-chart .card_cont .card-inner { display: block; } + +/* v21:卡片尺寸与 setCardDim({w:140, h:120}) 对齐 */ .kin-card { position: absolute; display: flex; flex-direction: column; align-items: center; gap: 0; + width: 140px; height: 120px; box-sizing: border-box; padding: 4px 4px 3px; text-align: center; background: var(--surface); @@ -499,19 +503,20 @@ textarea.input { min-height: 76px; resize: vertical; } transform: translate(-50%, -50%); /* 坐标给的是卡片中心 */ transition: border-color .15s ease, box-shadow .15s ease; overflow: hidden; + pointer-events: auto; /* 父 .card_cont 是 none,这里恢复 */ } .kin-card:hover { box-shadow: var(--shadow-l); border-color: var(--brand-2); text-decoration: none; } .kin-card .kin-ph { - width: 100%; height: 58px; border-radius: 7px; overflow: hidden; + width: 100%; height: 70px; border-radius: 7px; 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: 12.5px; font-weight: 600; line-height: 1.25; max-width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - margin-top: 3px; + margin-top: 4px; } /* 称呼在姓名下面一行,用品牌色的浅字(参考实现是「(称呼)」) */ .kin-card .kin-lb { @@ -529,6 +534,14 @@ textarea.input { min-height: 76px; resize: vertical; } /* 「待确认」(只填了称呼、没对应到真人):虚线 */ .kin-card.is-ph { border-style: dashed; background: #FCFAF6; } +/* family-chart 默认连线颜色改淡,与之前自研版一致。 + 库在 path 上写了 inline stroke="#fff",必须用 !important 覆盖。 */ +.kin-chart svg.main_svg .links_view path.link { + fill: none; + stroke: #C9BDA9 !important; + stroke-width: 1.6; +} + .tree-legend { font-size: 12px; color: var(--muted); margin: 14px 2px 0; } /* 详情页里的迷你家谱 */ diff --git a/templates/kin/tree.php b/templates/kin/tree.php index 0912269..de3e954 100644 --- a/templates/kin/tree.php +++ b/templates/kin/tree.php @@ -1,30 +1,15 @@ true]; -$bq = [$meId]; -while ($bq) { - $bid = (int)array_shift($bq); - foreach (array_merge($graph['parents'][$bid] ?? [], $graph['children'][$bid] ?? []) as $nb) { - $nb = (int)$nb; - if (!isset($blood[$nb])) { - $blood[$nb] = true; - $bq[] = $nb; - } - } -} - -$payloadUnits = []; -foreach ($units as $i => $u) { - $sorted = array_map('intval', $u['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'), - // ⚠️ 本家血亲标记:前端用它判定一户的「主父户」(见上方 $blood 的说明) - 'blood' => isset($blood[$id]), - // ⚠️ 长幼排序键:前端要靠它分「哥哥/弟弟」「姐姐/妹妹」。 - // 不用生日硬排(很多人没填生日),用 Kinship 里已有的 rankValueFor —— - // 它先看 birth_rank,再退回生日,最后用 id 兜底,保证顺序稳定。 - 'rank' => \App\Kinship::rankValueFor($people, $id), - ]; - } - if (!$cards) { - continue; - } - // ⚠️⚠️ 户内部:**本家血亲在前、配偶在后**(配偶排右手边,参考实现就是这样)。 + // ---- ② 树根 = 「我」往上走到的最高那一代 ---- // - // "血亲"不能只按"他有父母在本图上"判定 —— 一对夫妻**两边父母都可能在图上** - // (我的父母 + 妻子的父母),那样两边都算血亲,取到第一个就成了"妻子当主位、 - // 我排在她右边"(实测线上就是这个症状:红围脖@639 在我的左边)。 - // - // 正解:按「**与"我"的血缘距离**」判定 —— 这一户里离「我」更近的那一位才是主位。 - // 血亲集合 $blood 已在循环外算好(只走 parent/child 边的 BFS), - // 两边都在血亲集合里时,退到「与我的步数」比大小。 - $head = null; - $headScore = null; - foreach ($cards as $c) { - $cid = (int)$c['id']; - // 分数越小越"本家":0 = 血亲本身,1 = 配偶,2 = 其它 - $sc = isset($blood[$cid]) ? 0 : 1; - if ($head === null || $sc < $headScore) { - $head = $c; - $headScore = $sc; - } - } - if ($head !== null) { - $rest = []; - foreach ($cards as $c) { - if ((int)$c['id'] !== (int)$head['id']) { - $rest[] = $c; + // family-chart 画的是「树根的祖先链 + 后代 + 树根的兄弟姐妹 + 各人配偶」。 + // 树根 = 我 时,伯父/堂兄/嫂子/侄子 不在「我的后代」里,也不会被画出来; + // 把树根换成我的最高祖先,整棵树就变成「这位祖先的全部后代」, + // 上一代、这一代、下一代的血亲与旁支才能全部落进来。 + // 代价是「我」不再是几何中心 —— 用「回到我」按钮 + 加载后自动居中补回来。 + $rootId = (int)$meId; + for ($guard = 0; $guard < 8; $guard++) { + $up = \App\Kinship::parentIds($graph, $rootId); + $next = 0; + foreach ($up as $p) { + $p = (int)$p; + if (isset($onTree[$p]) && (int)$gen[$p] < (int)$gen[$rootId]) { + $next = $p; + break; } } - $cards = array_merge([$head], $rest); + if ($next === 0) { break; } + $rootId = $next; } - $kids = []; - foreach ($unitKids[$i] ?? [] as $cid) { - if (isset($people[$cid])) { - $kids[] = (int)$cid; + + // ---- ③ 连边:子女一律挂在「夫妻」上(normalizeLinks 的对称补齐)---- + // 这样库才会把连线从夫妻横线中点垂下来,母亲登录也能看到孩子。 + $links = \App\Kinship::normalizeLinks($graph, array_keys($onTree)); + + // ---- ④ 转成 family-chart 的 Datum 格式 ---- + $rankKey = static function (int $id) use ($people): string { + return \App\Kinship::rankValueFor($people, $id); + }; + + // 树根排最前(库默认以 data[0].id 为 main_id);其余按「代数 → 长幼」跟上 + $order = [$rootId]; + $rest = []; + foreach (array_keys($onTree) as $pid) { + $pid = (int)$pid; + if ($pid !== $rootId) { $rest[] = $pid; } + } + usort($rest, static function (int $a, int $b) use ($gen, $rankKey): int { + return ((int)$gen[$a] <=> (int)$gen[$b]) + ?: strcmp($rankKey($a), $rankKey($b)) + ?: ($a <=> $b); + }); + $order = array_merge($order, $rest); + + $mapIds = static function (array $ids) use ($onTree): array { + $out = []; + foreach ($ids as $x) { + $x = (int)$x; + if (isset($onTree[$x])) { $out[] = (string)$x; } } + return array_values(array_unique($out)); + }; + + $datum = []; + foreach ($order as $pid) { + if (!isset($people[$pid]) || !is_array($people[$pid])) { continue; } + $p = $people[$pid]; + + $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); + + $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, + ], + ]; } - usort($kids, static fn(int $a, int $b): int => strcmp($rankKey($a), $rankKey($b)) ?: ($a <=> $b)); - $payloadUnits[] = ['gen' => (int)$u['gen'], 'cards' => $cards, 'kids' => $kids]; + $canChart = count($datum) > 1; } -// 同代之间按「主位长幼」排序,保证顺序稳定 -usort($payloadUnits, 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 ($payloadUnits as $u) { - $totalPeople += count($u['cards']); -} -$canChart = $totalPeople > 1; - $meName = (string)($people[$meId]['name'] ?? '自己'); -$payloadJson = json_encode( - ['units' => $payloadUnits, 'me' => (int)$meId], +$datumJson = json_encode( + $datum ?? [], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT ); ?>

我的家谱

- 以我为中心 · 上一代 / 这一代 / 下一代所有人 · 子女从夫妻横线中点连出 + 以我为中心 · 上下一共五代 · 夫妻并排,子女从两人之间的连线连出
@@ -173,42 +143,31 @@ $payloadJson = json_encode(
-
+

- 每代一行,一代里按「户」排开;一对夫妻之间有一条短横线, - 他们的子女一律从这条横线的正中间垂下来(不管是谁登录,孩子都挂在这对夫妻上), - 再由横梁分到每个孩子。 + 每代一行;一对夫妻之间有一条短横线, + 他们的子女一律从这条横线的正中间垂下来(不管是谁登录,孩子都挂在这对夫妻上)。 「待确认」= 只填了称呼、还没对应到真实的人(虚线卡片)。 可以**拖拽平移**、**滚轮缩放**;点「回到我」把镜头挪回你自己,点卡片进对应成员页。 +
注:本图由 family-chart 绘制。免费版只画「同一位祖先的血脉后代 + 各人配偶」, + 妻子那侧的岳父母、舅子等姻亲不在图内(在名册与成员详情页仍可看到)。

+ - \ No newline at end of file +