,
+ 我们只接管最里面那层(.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(