2026-09-30 08:13:07 +08:00
|
|
|
|
<?php
|
|
|
|
|
|
/**
|
2026-10-02 19:24:08 +08:00
|
|
|
|
* 我的家谱:以我为中心,上一代 / 这一代 / 下一代所有有关系的人。
|
2026-09-30 08:13:07 +08:00
|
|
|
|
*
|
2026-10-02 19:24:08 +08:00
|
|
|
|
* v21 —— 回退到 family-chart 库(用户 2026-10-02 明确要求)。
|
2026-10-01 17:42:15 +08:00
|
|
|
|
*
|
2026-10-02 19:24:08 +08:00
|
|
|
|
* 之前自研布局叠了多层修补后位置漂移,用户决定还是交给成熟库来做。
|
|
|
|
|
|
* 数据准备要点:
|
|
|
|
|
|
* · 用 familyMap 把「上两代 / 本代 / 下两代」所有有关系的人(含姻亲旁支)全捞出来;
|
|
|
|
|
|
* · 转成 family-chart 的 Datum 格式 {id, data, rels: {parents, spouses, children}};
|
|
|
|
|
|
* · 「我」排数组第一位(库把 data[0].id 当 main_id);
|
|
|
|
|
|
* · 子女按长幼排序后写进 rels.children,库按数组顺序摆左右。
|
2026-09-30 08:13:07 +08:00
|
|
|
|
*/
|
|
|
|
|
|
|
2026-10-02 10:26:14 +08:00
|
|
|
|
$graph = $graph ?? \App\Kinship::graph((int)\App\Auth::familyId());
|
|
|
|
|
|
$people = $people ?? [];
|
2026-10-02 09:28:07 +08:00
|
|
|
|
|
2026-10-02 20:09:28 +08:00
|
|
|
|
// ---- ① 以我为中心,检索**所有有关系的人**(不设代数上限)----
|
|
|
|
|
|
// ⚠️ 用户明确要求「把以我为中心的关联数据全部取出来」。
|
|
|
|
|
|
// familyMap 的 $up/$down 只影响**代数标号**(超出就压到边界那一档),
|
|
|
|
|
|
// 遍历本身是走遍整个连通图的 —— 所以这两个值不放宽的话,
|
|
|
|
|
|
// 曾祖辈与祖辈会被压成同一代,排序就乱了。给足余量。
|
2026-10-03 08:18:05 +08:00
|
|
|
|
// ⚠️ v24 第 12 条:中心是 $focusId(默认=我)。点姻亲入口进来时,中心换成那个人,
|
|
|
|
|
|
// 这样 TA 那一侧的姻亲(岳父母等)才画得出来。
|
|
|
|
|
|
$focusId = (int)($focusId ?? $meId);
|
|
|
|
|
|
if (!isset($people[$focusId]) || !is_array($people[$focusId])) {
|
|
|
|
|
|
$focusId = (int)$meId;
|
|
|
|
|
|
}
|
|
|
|
|
|
$map = \App\Kinship::familyMap((int)\App\Auth::familyId(), $focusId, $people, 9, 9);
|
2026-10-02 10:26:14 +08:00
|
|
|
|
$gen = $map['gen'];
|
|
|
|
|
|
$units = $map['units'];
|
|
|
|
|
|
$unitOf = $map['unit_of'];
|
2026-10-01 17:42:15 +08:00
|
|
|
|
|
2026-10-03 08:18:05 +08:00
|
|
|
|
if (!isset($people[$focusId]) || !is_array($people[$focusId])) {
|
2026-10-02 19:24:08 +08:00
|
|
|
|
$canChart = false;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
$onTree = array_fill_keys(array_map('intval', array_keys($gen)), true);
|
2026-10-02 09:28:07 +08:00
|
|
|
|
|
2026-10-02 19:24:08 +08:00
|
|
|
|
// ---- ② 树根 = 「我」往上走到的最高那一代 ----
|
2026-10-02 13:38:34 +08:00
|
|
|
|
//
|
2026-10-02 19:24:08 +08:00
|
|
|
|
// family-chart 画的是「树根的祖先链 + 后代 + 树根的兄弟姐妹 + 各人配偶」。
|
|
|
|
|
|
// 树根 = 我 时,伯父/堂兄/嫂子/侄子 不在「我的后代」里,也不会被画出来;
|
|
|
|
|
|
// 把树根换成我的最高祖先,整棵树就变成「这位祖先的全部后代」,
|
|
|
|
|
|
// 上一代、这一代、下一代的血亲与旁支才能全部落进来。
|
|
|
|
|
|
// 代价是「我」不再是几何中心 —— 用「回到我」按钮 + 加载后自动居中补回来。
|
2026-10-03 08:18:05 +08:00
|
|
|
|
$rootId = (int)$focusId;
|
2026-10-02 19:24:08 +08:00
|
|
|
|
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;
|
2026-10-02 12:48:33 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-10-02 19:24:08 +08:00
|
|
|
|
if ($next === 0) { break; }
|
|
|
|
|
|
$rootId = $next;
|
2026-10-02 12:48:33 +08:00
|
|
|
|
}
|
2026-10-02 19:24:08 +08:00
|
|
|
|
|
|
|
|
|
|
// ---- ③ 连边:子女一律挂在「夫妻」上(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; }
|
2026-10-01 19:35:48 +08:00
|
|
|
|
}
|
2026-10-02 19:24:08 +08:00
|
|
|
|
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),
|
2026-10-03 08:18:05 +08:00
|
|
|
|
// 「我」在图上高亮;中心那个人另有标记($focusId)
|
|
|
|
|
|
'me' => $pid === (int)$focusId,
|
|
|
|
|
|
'is_me_viewer' => $pid === (int)$meId,
|
2026-10-02 19:24:08 +08:00
|
|
|
|
'ph' => (int)($p['is_placeholder'] ?? 0) === 1,
|
|
|
|
|
|
],
|
|
|
|
|
|
'rels' => [
|
|
|
|
|
|
'parents' => $parents,
|
|
|
|
|
|
'spouses' => $spouses,
|
|
|
|
|
|
'children' => $kids,
|
|
|
|
|
|
],
|
|
|
|
|
|
];
|
2026-10-02 10:26:14 +08:00
|
|
|
|
}
|
2026-10-02 09:28:07 +08:00
|
|
|
|
|
2026-10-03 15:57:56 +08:00
|
|
|
|
// ⚠️ v24 第 12 条曾在每人数据上挂一个 `branch`("他的姻亲没画在本图上"的人名),
|
|
|
|
|
|
// 用来给卡片加一个"⇄ 姻亲"入口。用户 2026-10-03 第 7 条把那个按钮撤了
|
|
|
|
|
|
// (改成"点姓名 = 以他为中心"),所以这份计算**连同渲染一起删掉了** ——
|
|
|
|
|
|
// 留着只会是"服务端算了一个没人用的字段",日后读代码的人还得反查它干嘛的。
|
|
|
|
|
|
// (如果哪天又要"姻亲入口",从 git 历史里捞这一段即可。)
|
2026-10-03 08:18:05 +08:00
|
|
|
|
|
2026-10-02 19:24:08 +08:00
|
|
|
|
$canChart = count($datum) > 1;
|
2026-10-01 19:35:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-02 10:26:14 +08:00
|
|
|
|
$meName = (string)($people[$meId]['name'] ?? '自己');
|
2026-10-02 19:24:08 +08:00
|
|
|
|
$datumJson = json_encode(
|
|
|
|
|
|
$datum ?? [],
|
2026-10-01 19:35:48 +08:00
|
|
|
|
JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT
|
|
|
|
|
|
);
|
2026-09-30 08:13:07 +08:00
|
|
|
|
?>
|
|
|
|
|
|
|
2026-10-03 11:30:48 +08:00
|
|
|
|
<?php /* 用户 2026-10-03 第 2 条:标题下的那句说明**去掉**;
|
|
|
|
|
|
右下角的「返回我的家谱」也去掉 —— 与页面里那个工具条重复,
|
|
|
|
|
|
会出现两个"回到我的家谱"(用户明确点名)。现在整页只有标题。 */ ?>
|
2026-09-30 08:13:07 +08:00
|
|
|
|
<div class="page-head">
|
2026-10-03 08:18:05 +08:00
|
|
|
|
<h1><?= $focusId === (int)$meId ? '我的家谱' : e((string)($people[$focusId]['name'] ?? '') . ' 的家谱') ?></h1>
|
2026-09-30 08:13:07 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-10-01 19:35:48 +08:00
|
|
|
|
<?php if (!$canChart): ?>
|
2026-09-30 08:13:07 +08:00
|
|
|
|
<div class="empty">
|
|
|
|
|
|
<b>还没连上任何亲属关系</b>
|
2026-10-02 10:26:14 +08:00
|
|
|
|
到「<?= e($meName) ?>」的资料页,把父母、配偶、子女填进去,
|
2026-09-30 08:13:07 +08:00
|
|
|
|
这张家谱就会自动长出来。兄弟姐妹不用单独填 —— 只要你们有同一对父母,就会自动认出来。
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<?php else: ?>
|
|
|
|
|
|
|
2026-10-03 11:30:48 +08:00
|
|
|
|
<?php /* 用户 2026-10-03 第 2 条:「看全部 / 回到我 / 回到我的家谱」这些按钮
|
|
|
|
|
|
全部去掉。镜头由库自动 fit(updateTree 的 tree_position: 'fit'),
|
|
|
|
|
|
用户要调整时直接拖拽/滚轮即可,不需要常驻按钮。 */
|
|
|
|
|
|
/* ⚠️ 顺带说明:品牌名「家谱」已从导航条撤走,但**模块本身还在** ——
|
|
|
|
|
|
个人设置里能勾回来,且 URL 直接访问 r=kin 永远有效。
|
|
|
|
|
|
这一点写在这里是为了防止有人"顺手把 kin 路由删掉"。 */ ?>
|
2026-10-02 09:28:07 +08:00
|
|
|
|
|
2026-10-02 20:09:28 +08:00
|
|
|
|
<!-- ⚠️ `f3` 这个类名是**库的 CSS 作用域**,不是我们起的名字。
|
|
|
|
|
|
官方示例就是 `<div id="FamilyChart" class="f3">` + 引 family-chart.css;
|
|
|
|
|
|
少了它,库里 `.f3 svg.main_svg{width:100%;height:100%}` 这条撑满规则不生效,
|
|
|
|
|
|
SVG 会退化成浏览器默认的 300×150(连线全被裁掉,而 HTML 卡片层不受影响 →
|
|
|
|
|
|
表现为「卡在、线没了」)。 -->
|
2026-10-03 08:18:05 +08:00
|
|
|
|
<div class="kin-chart f3" id="kinChart" data-datum="<?= e($datumJson) ?>"
|
|
|
|
|
|
data-focus="<?= e((string)$focusId) ?>" data-me-viewer="<?= e((string)$meId) ?>"></div>
|
2026-09-30 08:13:07 +08:00
|
|
|
|
|
2026-10-03 11:30:48 +08:00
|
|
|
|
|
2026-10-01 12:11:43 +08:00
|
|
|
|
|
2026-10-02 20:09:28 +08:00
|
|
|
|
<!-- 官方用法是「容器带 class="f3" + 引 family-chart.css」(见 family-chart 官方示例)。
|
|
|
|
|
|
之前没引这份 CSS,靠自己在 app.css 里手补 `.kin-chart svg.main_svg{width/height:100%}`
|
|
|
|
|
|
绕过去 —— 属于"调用方法不对"。现在按官方来。
|
|
|
|
|
|
库的深色主题(--male-color / --background-color…)在 app.css 里覆盖成家书的宣纸色。 -->
|
|
|
|
|
|
<link rel="stylesheet" href="<?= e(asset('/assets/vendor/family-chart/family-chart.css')) ?>">
|
2026-10-01 19:35:48 +08:00
|
|
|
|
<script src="<?= e(asset('/assets/vendor/family-chart/d3.min.js')) ?>"></script>
|
2026-10-02 19:24:08 +08:00
|
|
|
|
<script src="<?= e(asset('/assets/vendor/family-chart/family-chart.min.js')) ?>"></script>
|
2026-10-01 19:35:48 +08:00
|
|
|
|
<script>
|
|
|
|
|
|
(function () {
|
|
|
|
|
|
var root = document.getElementById('kinChart');
|
2026-10-02 19:24:08 +08:00
|
|
|
|
if (!root || typeof window.f3 === 'undefined') { return; }
|
2026-10-01 19:35:48 +08:00
|
|
|
|
|
2026-10-02 19:24:08 +08:00
|
|
|
|
var raw;
|
2026-10-01 19:35:48 +08:00
|
|
|
|
try {
|
2026-10-02 19:24:08 +08:00
|
|
|
|
raw = JSON.parse(root.getAttribute('data-datum') || '[]');
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
raw = [];
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!raw.length) { return; }
|
2026-10-01 19:35:48 +08:00
|
|
|
|
|
|
|
|
|
|
function esc(s) {
|
|
|
|
|
|
return String(s == null ? '' : s)
|
|
|
|
|
|
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
|
|
|
|
.replace(/"/g, '"').replace(/'/g, ''');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-02 19:24:08 +08:00
|
|
|
|
var chart = f3.createChart('#kinChart', raw)
|
|
|
|
|
|
.setTransitionTime(600)
|
|
|
|
|
|
.setOrientationVertical()
|
|
|
|
|
|
.setShowSiblingsOfMain(true)
|
2026-10-02 20:09:28 +08:00
|
|
|
|
// ⚠️ 官方示例**不设**祖先/后代的代数上限 —— 库在"未设置"时
|
|
|
|
|
|
// 会跳过深度截断,等于**不限代数**,有多少画多少。
|
|
|
|
|
|
// 之前写死了具体代数,真实家庭一超过就被砍掉。
|
|
|
|
|
|
// 用户要求「全部取出来」,所以这里也不设。
|
2026-10-03 11:30:48 +08:00
|
|
|
|
.setCardXSpacing(160)
|
2026-10-02 19:24:08 +08:00
|
|
|
|
.setCardYSpacing(180);
|
2026-10-01 19:35:48 +08:00
|
|
|
|
|
2026-10-02 19:24:08 +08:00
|
|
|
|
chart.setCardHtml()
|
|
|
|
|
|
.setStyle('rect')
|
2026-10-03 11:30:48 +08:00
|
|
|
|
.setCardDim({ w: 118, h: 132, mini_tree: false })
|
2026-10-02 19:24:08 +08:00
|
|
|
|
.setCardInnerHtmlCreator(function (d) {
|
|
|
|
|
|
var p = (d.data && d.data.data) || {};
|
|
|
|
|
|
var id = (d.data && d.data.id) || '';
|
|
|
|
|
|
var cls = 'kin-card' + (p.me ? ' is-me' : '') + (p.ph ? ' is-ph' : '');
|
2026-10-03 15:57:56 +08:00
|
|
|
|
/* 用户 2026-10-03(第 6 条):
|
2026-10-03 11:30:48 +08:00
|
|
|
|
① 卡片**不再整张是链接** —— 原来整卡是 <a>,右下角那个小按钮也在
|
|
|
|
|
|
<a> 里,手指/鼠标稍偏一点就先触发外层链接跳去成员页(用户反馈
|
2026-10-03 15:57:56 +08:00
|
|
|
|
"容易误触"的根因)。现在卡片是 <div>,点哪儿都不会误跳。
|
2026-10-03 11:30:48 +08:00
|
|
|
|
② 头像外框改**长方形**、宽度贴合头像本身(CSS 里 .kin-ph 定宽,
|
|
|
|
|
|
img 用 object-fit: contain 不裁切),信息在头像**下方**。
|
2026-10-03 15:57:56 +08:00
|
|
|
|
用户 2026-10-03(第 7 条):**撤掉右下角的「以 TA 为中心」按钮**,
|
|
|
|
|
|
改成把卡片分成**两个点击区**:
|
|
|
|
|
|
· 头像 → 进这个人的资料页;
|
|
|
|
|
|
· 姓名 → 以他为中心重画一张家谱。
|
|
|
|
|
|
⚠️ 两个都是 <a>(不是一个 a + 一个 button):
|
|
|
|
|
|
"点名字换中心"本质就是**换个 URL 重新渲染**,用链接最自然 ——
|
|
|
|
|
|
能中键新窗口打开、能右键复制地址、没有 JS 也能用。
|
|
|
|
|
|
原来那个 <button> 要手动绑 click + 键盘事件 + 三处 stopPropagation,
|
|
|
|
|
|
全是"用按钮做链接"的代价;换成 <a> 之后这些代码整段删掉。 */
|
2026-10-03 11:30:48 +08:00
|
|
|
|
return '<div class="' + cls + '" data-kin-id="' + esc(id) + '">'
|
2026-10-03 15:57:56 +08:00
|
|
|
|
+ '<a class="kin-ph" href="/index.php?r=people/show&id='
|
|
|
|
|
|
+ encodeURIComponent(id) + '" title="看 ' + esc(p.name) + ' 的资料">'
|
|
|
|
|
|
+ '<img src="' + esc(p.avatar) + '" alt=""></a>'
|
|
|
|
|
|
+ '<a class="kin-nm" href="/index.php?r=kin&focus='
|
|
|
|
|
|
+ encodeURIComponent(id) + '" title="以 ' + esc(p.name) + ' 为中心看家谱">'
|
2026-10-03 11:30:48 +08:00
|
|
|
|
+ esc(p.name) + '</a>'
|
2026-10-02 19:24:08 +08:00
|
|
|
|
+ '<span class="kin-lb">' + esc(p.label) + '</span>'
|
|
|
|
|
|
+ (p.ph ? '<span class="kin-mark">待确认</span>' : '')
|
2026-10-03 15:57:56 +08:00
|
|
|
|
// 两张提示分挂到头像/姓名下面,**悬停时才显形**(见 CSS 的 .kin-hint):
|
|
|
|
|
|
// 这样"点哪儿去哪儿"一眼可辨,不悬停时又不占地方。
|
|
|
|
|
|
+ '<span class="kin-hint kin-hint-ph">资料</span>'
|
|
|
|
|
|
+ '<span class="kin-hint kin-hint-nm">以他为中心</span>'
|
2026-10-03 11:30:48 +08:00
|
|
|
|
+ '</div>';
|
2026-10-02 12:48:33 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-10-02 19:24:08 +08:00
|
|
|
|
chart.editTree().setCanAdd(false);
|
|
|
|
|
|
chart.setSingleParentEmptyCard(false);
|
2026-10-02 12:48:33 +08:00
|
|
|
|
|
2026-10-02 19:24:08 +08:00
|
|
|
|
chart.updateTree({ initial: true, tree_position: 'fit' });
|
|
|
|
|
|
window.kinChart = chart;
|
2026-10-02 12:48:33 +08:00
|
|
|
|
|
2026-10-02 19:24:08 +08:00
|
|
|
|
// family-chart 的 zoom 行为绑在 #f3Canvas 上,SVG 与 HTML 卡片层由库的 onZoom 同步。
|
|
|
|
|
|
// 必须拿到 f3Canvas 才能正确驱动镜头;直接操作 svg.__zoomObj 或只动 svg 都会让
|
|
|
|
|
|
// HTML 卡片层不同步。
|
|
|
|
|
|
var f3Canvas = root.querySelector('#f3Canvas');
|
|
|
|
|
|
function zoomObj() {
|
|
|
|
|
|
if (!f3Canvas) { f3Canvas = root.querySelector('#f3Canvas'); }
|
|
|
|
|
|
// __zoomObj 挂在 #f3Canvas 上;库的 d() helper 也会往父节点找
|
|
|
|
|
|
var el = f3Canvas && f3Canvas.__zoomObj ? f3Canvas
|
|
|
|
|
|
: (f3Canvas && f3Canvas.parentNode && f3Canvas.parentNode.__zoomObj ? f3Canvas.parentNode : null);
|
|
|
|
|
|
return el ? el.__zoomObj : null;
|
2026-10-02 17:35:27 +08:00
|
|
|
|
}
|
2026-10-02 19:24:08 +08:00
|
|
|
|
function applyTransform(t) {
|
|
|
|
|
|
var z = zoomObj(), d3 = window.d3;
|
|
|
|
|
|
if (!z || !d3 || !f3Canvas) return;
|
|
|
|
|
|
d3.select(f3Canvas).call(z.transform, t);
|
2026-10-02 09:28:07 +08:00
|
|
|
|
}
|
2026-10-02 19:24:08 +08:00
|
|
|
|
// 把镜头挪到指定人:用库算出的树坐标,固定比例 0.9,避免「看全部」时缩得太小
|
2026-10-02 09:28:07 +08:00
|
|
|
|
function centerOn(id) {
|
2026-10-02 19:24:08 +08:00
|
|
|
|
var d3 = window.d3;
|
|
|
|
|
|
if (!d3) return;
|
|
|
|
|
|
var tree = chart.store.getTree();
|
|
|
|
|
|
if (!tree || !tree.data) return;
|
|
|
|
|
|
var node = tree.data.find(function (n) { return n.data && n.data.id === id; });
|
|
|
|
|
|
if (!node) return;
|
|
|
|
|
|
var rr = root.getBoundingClientRect();
|
|
|
|
|
|
var k = 0.9;
|
|
|
|
|
|
var tx = rr.width / 2 - node.x * k;
|
|
|
|
|
|
var ty = rr.height / 2 - node.y * k;
|
|
|
|
|
|
// d3 zoom 的 {x,y} 是「缩放后再平移」的屏幕像素偏移,
|
|
|
|
|
|
// 所以先 translate(tx,ty) 再 scale(k) 才能得到 screen = node * k + offset。
|
|
|
|
|
|
applyTransform(d3.zoomIdentity.translate(tx, ty).scale(k));
|
2026-10-02 09:28:07 +08:00
|
|
|
|
}
|
|
|
|
|
|
window.kinCenterOn = centerOn;
|
|
|
|
|
|
|
2026-10-03 08:18:05 +08:00
|
|
|
|
var focusId = root.getAttribute('data-focus') || '';
|
|
|
|
|
|
var meId = root.getAttribute('data-me-viewer') || focusId;
|
|
|
|
|
|
// ⚠️ 中心是别人时,「回到我」要跳到 focus=我 的图,而不是在图里找我的坐标
|
|
|
|
|
|
var centerTarget = root.getAttribute('data-me-viewer') || focusId;
|
2026-10-02 19:24:08 +08:00
|
|
|
|
// fit 动画完成后把镜头挪到「我」(树根是最高祖先,我不再是几何中心)
|
|
|
|
|
|
if (meId) {
|
2026-10-03 08:18:05 +08:00
|
|
|
|
setTimeout(function () { centerOn(focusId); }, 900);
|
2026-10-02 19:24:08 +08:00
|
|
|
|
// 万一 zoom 对象还没初始化好,再补一次
|
2026-10-03 08:18:05 +08:00
|
|
|
|
setTimeout(function () { centerOn(focusId); }, 1400);
|
2026-10-02 19:24:08 +08:00
|
|
|
|
}
|
2026-10-02 09:28:07 +08:00
|
|
|
|
|
2026-10-03 11:30:48 +08:00
|
|
|
|
/* 用户 2026-10-03 第 2 条:工具条三个按钮(看全部/回到我/回到我的家谱)已撤。
|
2026-10-03 15:57:56 +08:00
|
|
|
|
镜头由库自动 fit。
|
|
|
|
|
|
用户 2026-10-03 第 7 条:「以 TA 为中心」的按钮也撤了 —— 现在**点姓名**就是
|
|
|
|
|
|
以他为中心(那个 <a> 直接指向 ?r=kin&focus=<id>,是普通的页面跳转)。
|
|
|
|
|
|
⚠️ 原来这里有一大段"给那个小按钮绑 click + keydown + mousedown/pointerdown/
|
|
|
|
|
|
touchstart 三处阻止冒泡"的代码,那是**用按钮干链接的活**才需要的补丁
|
|
|
|
|
|
(防止先触发外层链接)。换成 <a> 之后整段删掉:
|
|
|
|
|
|
少一段代码、少一类 bug,键盘与中键打开也天然可用。 */
|
2026-10-03 08:18:05 +08:00
|
|
|
|
|
2026-10-01 19:35:48 +08:00
|
|
|
|
})();
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
2026-10-02 19:24:08 +08:00
|
|
|
|
<?php endif; ?>
|