v15: 家谱重写 —— 以我为中心检索全体亲属 + 自己排版画线

用户 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(旧解析器在新页面上只会解析出空)
This commit is contained in:
2026-10-02 10:26:14 +08:00
parent 71a7ec35a0
commit 80f5c4f0fc
4 changed files with 444 additions and 293 deletions
+39 -45
View File
@@ -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 实现。 */
⚠️ 卡片/连线都不是我们生成的:卡片是库建的真实 <div>(.card_cont > .card > 我们的
.kin-card),连线是 svg 里的 <path class="link">。改版式只能从这两个钩子下手。 */
/* 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; }