2026-09-30 08:13:07 +08:00
|
|
|
|
<?php
|
|
|
|
|
|
/**
|
2026-10-01 19:35:48 +08:00
|
|
|
|
* 我的家谱:以我为中心,向上向下各三代。
|
2026-09-30 08:13:07 +08:00
|
|
|
|
*
|
2026-10-01 19:35:48 +08:00
|
|
|
|
* **v13 —— 改用成熟的第三方库 family-chart(D3 生态,MIT)来画。**
|
2026-10-01 17:42:15 +08:00
|
|
|
|
*
|
2026-10-01 19:35:48 +08:00
|
|
|
|
* 为什么放弃自研(v12 的「单一网格」也救不回来):
|
|
|
|
|
|
* v11「每代各一个网格」的列宽由内容决定,各代对不齐;v12 收成「整棵树一个 grid」,
|
|
|
|
|
|
* 几何上确实对齐了,但仍有一个**结构性死结**:连线的横杆是**网格项**,只能横跨
|
|
|
|
|
|
* 「从父母那一列到子女那一列」的整段列区间。于是只要中间夹着**跟这条血缘无关**的人
|
|
|
|
|
|
* (最典型的是配偶带进来的岳父母),横杆就会**从他们卡片中间穿过去**。
|
|
|
|
|
|
* 这不是调样式能修的 —— 横杆的跨度由列位决定,而列位是为「同代并排」服务的,
|
|
|
|
|
|
* 两套诉求在同一张网格里没法同时满足。
|
2026-10-01 17:42:15 +08:00
|
|
|
|
*
|
2026-10-01 19:35:48 +08:00
|
|
|
|
* 换成库之后:
|
|
|
|
|
|
* · 布局(谁放哪、怎么折线、怎么避让)全部交给库的树算法,不再由我们用网格硬凑;
|
|
|
|
|
|
* · 库只画**本家血脉 + 配偶**,不画配偶的父母 —— 这正是「横杆穿卡片」的根源被消掉的原因,
|
|
|
|
|
|
* 语义上也算合理(姻亲本来就不是本家的血亲,v11 起就单独标着「姻亲」两个字)。
|
|
|
|
|
|
* · 自带拖拽平移 / 滚轮缩放,家里人多了不用我们做横向滚动。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 接入时踩过的三个坑(都写进注释,别再踩):
|
|
|
|
|
|
* ① 库把**数据数组里的第一个人**当默认"主人物"(createChart 里 `main_id = data[0].id`),
|
|
|
|
|
|
* 所以「我」必须排在数组最前面,否则整棵树会以别人为中心。
|
|
|
|
|
|
* ② 卡片回调拿到的是 `TreeDatum`,它 `.data` 是**整个 Datum**({id,data,rels}),
|
|
|
|
|
|
* 我们自己塞的字段在 **`d.data.data.xxx`**(双层)。写一层就得到空白卡片。
|
|
|
|
|
|
* ③ 库的 family-chart.css **不必引入**:实测库里那些规则**全部挂在 `.f3` 下**,
|
|
|
|
|
|
* 而运行时并不会生成任何 `.f3` 元素(卡片的定位全靠 inline style:
|
|
|
|
|
|
* `.card_cont{position:absolute;transform:translate(...)}`、
|
|
|
|
|
|
* `.cards_view{transform:scale(...)}`)—— 引进来也是死文件,反而多了 16KB。
|
|
|
|
|
|
* 真正需要的三条(svg 撑满、连线颜色、卡片观感)我们自己写,见 app.css 的 `.kin-chart`。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 称呼、头像、「本人」高亮、待确认这些语义都由 App\Kinship::label() / person 记录决定,一字未改。
|
2026-09-30 08:13:07 +08:00
|
|
|
|
*/
|
2026-10-01 19:35:48 +08:00
|
|
|
|
$nodes = $tree['nodes'];
|
|
|
|
|
|
$me = $nodes[$meId] ?? null;
|
2026-09-30 08:13:07 +08:00
|
|
|
|
|
2026-10-02 09:28:07 +08:00
|
|
|
|
// ---- v14:先圈定「上 / 本 / 下三代里所有有关系的人」----
|
|
|
|
|
|
//
|
|
|
|
|
|
// ⚠️ 这是本轮重写的核心(用户第 5 条:「显示不齐全…以本人为中心去搜索,只要有关系的
|
|
|
|
|
|
// 都检索出来进行展示」)。旧写法把**全名册**塞给库,看着多,其实库只画
|
|
|
|
|
|
// 「从 main_id 出发的层次结构」,横向的堂表亲、配偶那边的旁支照样进不来,
|
|
|
|
|
|
// 而树上又挂了一堆根本没关系的人(看着像"家谱里混进了外人")。
|
|
|
|
|
|
// 现在先自己按**代数**筛选(focusMap),再交给库 —— 库拿到的人就是该出现的人。
|
|
|
|
|
|
$focusGen = \App\Kinship::focusMap($graph, (int)$meId, $people, 2, 2);
|
|
|
|
|
|
if (!isset($focusGen[(int)$meId])) {
|
|
|
|
|
|
$focusGen[(int)$meId] = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
$focusIds = array_map('intval', array_keys($focusGen));
|
|
|
|
|
|
|
|
|
|
|
|
// ---- 树根 = 「我」往上走到的最高那一代 ----
|
|
|
|
|
|
//
|
|
|
|
|
|
// ⚠️⚠️ 这是 v14 最关键的一处实测结论(_fb_kin_probe.py / probe3 跑出来的):
|
|
|
|
|
|
// family-chart 画的是「**树根**的祖先链 + 后代 + 树根的兄弟姐妹 + 各人的配偶」。
|
|
|
|
|
|
// 树根 = 我 时,**伯父 / 堂兄 / 嫂子 / 侄子 一个都不画**(实测 12 人只画出 8 人)——
|
|
|
|
|
|
// 因为堂兄是"我伯父的孩子",不在"我的后代"里,也不是"我的兄弟姐妹"。
|
|
|
|
|
|
// 把树根换成**我的最高祖先**,整棵树就变成"这位祖先的全部后代",
|
|
|
|
|
|
// 于是上一代(伯叔姑舅姨)、这一代(堂表亲)、下一代(侄甥)**全部**落进树里
|
|
|
|
|
|
// (实测 12 人全画出,且按辈分正确分层)。
|
|
|
|
|
|
// 这正是用户说的「显示不齐全…以本人为中心去搜索,只要有关系的都检索出来」。
|
|
|
|
|
|
//
|
|
|
|
|
|
// ⚠️ 代价是"我"不再是几何中心 —— 用下面的「定位到我」按钮 + 加载后自动居中补回来。
|
|
|
|
|
|
$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($focusGen[$p]) && (int)$focusGen[$p] < (int)$focusGen[$rootId]) {
|
|
|
|
|
|
$next = $p;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
2026-09-30 08:13:07 +08:00
|
|
|
|
}
|
2026-10-02 09:28:07 +08:00
|
|
|
|
if ($next === 0) {
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
$rootId = $next;
|
2026-10-01 12:11:43 +08:00
|
|
|
|
}
|
2026-10-01 17:42:15 +08:00
|
|
|
|
|
2026-10-02 09:28:07 +08:00
|
|
|
|
// ---- 连边:子女一律挂在「夫妻」上(normalizeLinks 的对称补齐)----
|
|
|
|
|
|
// 见 App\Kinship::normalizeLinks 的注释:孩子同时挂到父母双方、双方也都有这个孩子,
|
|
|
|
|
|
// 库才会把连线从**夫妻之间那条横线**的中点拉下来 —— 否则就是"从父亲一个人身上
|
|
|
|
|
|
// 垂下来一根线",母亲那条线永远连不到孩子(用户点名的重大 bug)。
|
|
|
|
|
|
$links = \App\Kinship::normalizeLinks($graph, $focusIds);
|
|
|
|
|
|
|
|
|
|
|
|
// 同一代里按长幼排(长子左、次子右);长幼键取不到时退到 id,保证顺序稳定
|
|
|
|
|
|
$rankKey = static function (int $id) use ($people): string {
|
|
|
|
|
|
return \App\Kinship::rankValueFor($people, $id);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// ---- 转成 family-chart 需要的 Datum 格式 ----
|
|
|
|
|
|
// ⚠️ **树根**排最前(见上面坑①:`main_id = data[0].id`)。其余按「代数 → 长幼」跟上。
|
|
|
|
|
|
$order = [$rootId];
|
|
|
|
|
|
$rest = [];
|
|
|
|
|
|
foreach ($focusIds as $pid) {
|
|
|
|
|
|
if ((int)$pid !== $rootId) {
|
|
|
|
|
|
$rest[] = (int)$pid;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
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 = [];
|
2026-10-01 19:35:48 +08:00
|
|
|
|
foreach ($order as $pid) {
|
|
|
|
|
|
if (!isset($people[$pid]) || !is_array($people[$pid])) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
$p = $people[$pid];
|
2026-10-01 17:42:15 +08:00
|
|
|
|
|
2026-10-02 09:28:07 +08:00
|
|
|
|
$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 09:28:07 +08:00
|
|
|
|
return array_values(array_unique($out));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
$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);
|
2026-10-01 19:35:48 +08:00
|
|
|
|
|
|
|
|
|
|
// 库认 M / F / U 三种(决定卡片底色)
|
|
|
|
|
|
$g = strtolower((string)($p['gender'] ?? ''));
|
|
|
|
|
|
$gender = $g === 'm' ? 'M' : ($g === 'f' ? 'F' : 'U');
|
|
|
|
|
|
|
|
|
|
|
|
$datum[] = [
|
|
|
|
|
|
'id' => (string)$pid,
|
|
|
|
|
|
'data' => [
|
|
|
|
|
|
'gender' => $gender,
|
2026-10-02 09:28:07 +08:00
|
|
|
|
'name' => \App\Nickname::display((int)$meId, $p) ?: (string)($p['name'] ?? ''),
|
2026-10-01 19:35:48 +08:00
|
|
|
|
'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,
|
|
|
|
|
|
],
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ⚠️ 名字是用户自己输入的:拼 HTML 前必须转义,否则有人把名字填成一段「能提前闭合
|
|
|
|
|
|
// 脚本块」的标签,就能从脚本里逃出去、把整页结构打断。
|
|
|
|
|
|
// ⚠️ 这里**刻意不写出那个闭合标签的字面量** —— 静态检查(check_inline_js.py)
|
|
|
|
|
|
// 是按原文找脚本块的,注释里写出来会被它当成真的脚本段去解析,报假错。
|
|
|
|
|
|
$datumJson = json_encode(
|
|
|
|
|
|
$datum,
|
|
|
|
|
|
JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
$canChart = count($datum) > 1;
|
2026-09-30 08:13:07 +08:00
|
|
|
|
?>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="page-head">
|
|
|
|
|
|
<h1>我的家谱</h1>
|
2026-10-02 09:28:07 +08:00
|
|
|
|
<span class="sub">以我为中心 · 上一代 / 这一代 / 下一代 · 夫妻并排,子女从两人之间的横线连出</span>
|
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>
|
|
|
|
|
|
到「<?= e($me['name'] ?? '自己') ?>」的资料页,把父母、配偶、子女填进去,
|
|
|
|
|
|
这张家谱就会自动长出来。兄弟姐妹不用单独填 —— 只要你们有同一对父母,就会自动认出来。
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<?php else: ?>
|
|
|
|
|
|
|
2026-10-01 19:35:48 +08:00
|
|
|
|
<?php
|
|
|
|
|
|
/* 家谱画布。
|
|
|
|
|
|
高度用 clamp 给:太矮时三代人挤成一团,太高又白占屏。
|
|
|
|
|
|
⚠️ 库初始化那一刻**容器必须已经有真实尺寸**:它按容器宽高算 fit 缩放,
|
|
|
|
|
|
还在 0×0 时初始化会一路缩到 0.4 倍,卡片小到看不清(实测踩过)。 */
|
|
|
|
|
|
?>
|
2026-10-02 09:28:07 +08:00
|
|
|
|
<div class="kin-tools">
|
|
|
|
|
|
<?php /* 树根是「最高祖先」之后,整棵树可能很大 —— 这两个按钮把「看全部」与
|
|
|
|
|
|
「回到我」分开,不然每次进来都要手动拖半天才能找到自己。 */ ?>
|
|
|
|
|
|
<button type="button" class="btn btn-ghost btn-sm" id="kinFit">看全部</button>
|
|
|
|
|
|
<button type="button" class="btn btn-ghost btn-sm" id="kinMe">回到我</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="kin-chart" id="kinChart" data-datum="<?= e($datumJson) ?>"
|
|
|
|
|
|
data-me="<?= e((string)$meId) ?>"></div>
|
2026-09-30 08:13:07 +08:00
|
|
|
|
|
2026-10-01 12:11:43 +08:00
|
|
|
|
<p class="tree-legend">
|
2026-10-02 09:28:07 +08:00
|
|
|
|
画的是**以我的最高祖先为根、本家血脉的整棵后代树**:父母与伯叔姑舅姨(上一代)、
|
|
|
|
|
|
我与兄弟姐妹和堂表亲及各自的配偶(这一代)、他们的孩子(下一代)都在上面。
|
2026-10-01 19:35:48 +08:00
|
|
|
|
「待确认」= 只填了称呼、还没对应到真实的人(虚线卡片)。
|
2026-10-02 09:28:07 +08:00
|
|
|
|
夫妻并排,**子女一律从两人之间的横线连出** —— 不管是谁登录,孩子都挂在这对夫妻上。
|
|
|
|
|
|
可以**拖拽平移**、**滚轮缩放**;点「回到我」把镜头挪回你自己,点卡片进对应成员页。
|
2026-10-01 12:11:43 +08:00
|
|
|
|
</p>
|
|
|
|
|
|
|
2026-10-01 19:35:48 +08:00
|
|
|
|
<script src="<?= e(asset('/assets/vendor/family-chart/d3.min.js')) ?>"></script>
|
|
|
|
|
|
<script src="<?= e(asset('/assets/vendor/family-chart/family-chart.min.js')) ?>"></script>
|
|
|
|
|
|
<script>
|
|
|
|
|
|
(function () {
|
|
|
|
|
|
var root = document.getElementById('kinChart');
|
|
|
|
|
|
if (!root || typeof f3 === 'undefined') { return; }
|
|
|
|
|
|
|
|
|
|
|
|
var raw;
|
|
|
|
|
|
try {
|
|
|
|
|
|
raw = JSON.parse(root.getAttribute('data-datum') || '[]');
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
raw = [];
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!raw.length) { return; }
|
|
|
|
|
|
|
|
|
|
|
|
// 名字是用户输入 —— 插进 innerHTML 前必须转义(库只负责摆放,不管转义)
|
|
|
|
|
|
function esc(s) {
|
|
|
|
|
|
return String(s == null ? '' : s)
|
|
|
|
|
|
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
|
|
|
|
.replace(/"/g, '"').replace(/'/g, ''');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var chart = f3.createChart('#kinChart', raw)
|
|
|
|
|
|
.setTransitionTime(600)
|
|
|
|
|
|
.setOrientationVertical()
|
|
|
|
|
|
.setShowSiblingsOfMain(true)
|
|
|
|
|
|
.setCardXSpacing(140)
|
|
|
|
|
|
.setCardYSpacing(140);
|
|
|
|
|
|
|
|
|
|
|
|
chart.setCardHtml()
|
|
|
|
|
|
.setStyle('rect')
|
|
|
|
|
|
.setCardDim({ w: 116, h: 104, mini_tree: false })
|
|
|
|
|
|
.setCardInnerHtmlCreator(function (d) {
|
|
|
|
|
|
// ⚠️ 双层:d.data 是整个 Datum,自定义字段在 d.data.data(见文件头坑②)
|
|
|
|
|
|
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' : '');
|
|
|
|
|
|
// ⚠️ 卡片本身就是个 <a>,直接跳成员页 —— **不要**去找库的点击回调。
|
|
|
|
|
|
// 踩过:写了 chart.setOnCardClick(...),而 chart 上根本没有这个方法
|
|
|
|
|
|
// (抛 TypeError),异常把后面的 updateTree() 一起带走了 →
|
|
|
|
|
|
// 整张家谱**一张卡都不画**(页面 200、容器有尺寸、看着"正常")。
|
|
|
|
|
|
// 用 <a> 件不依赖任何库 API,也不怕库版本变了改签名。
|
2026-10-02 09:28:07 +08:00
|
|
|
|
return '<a class="' + cls + '" data-kin-id="' + esc(id) + '" href="/index.php?r=people/show&id='
|
2026-10-01 19:35:48 +08:00
|
|
|
|
+ encodeURIComponent(id) + '">'
|
|
|
|
|
|
+ '<span class="kin-ph"><img src="' + esc(p.avatar) + '" alt=""></span>'
|
|
|
|
|
|
+ '<span class="kin-nm">' + esc(p.name) + '</span>'
|
|
|
|
|
|
+ '<span class="kin-lb">' + esc(p.label) + '</span>'
|
|
|
|
|
|
+ (p.ph ? '<span class="kin-mark">待确认</span>' : '')
|
|
|
|
|
|
+ '</a>';
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ⚠️⚠️ 关掉库自带的「+ 添加亲属」空卡。
|
|
|
|
|
|
// **setCanAdd 不在 chart 上,在 chart.editTree() 返回的对象上**(实测:
|
|
|
|
|
|
// chart 的原型上是 calculateKinships / updateTree / setCardHtml ...,
|
|
|
|
|
|
// 没有 setCanAdd;editTree() 返回的编辑树对象上才有)。
|
|
|
|
|
|
// 写成 chart.setCanAdd(false) 会抛 TypeError,异常把后面的 updateTree() 一起
|
|
|
|
|
|
// 带走 → 整张图**一张卡都不画**(页面照样 200、容器照样有尺寸,光看页面像"正常")。
|
|
|
|
|
|
// 这个坑在本文件里连着犯了三次(setOnCardClick / setCanAdd 链式调用 / 这一处),
|
|
|
|
|
|
// 所以:**新增库调用前先把方法名在浏览器里打印出来确认,别照文档或习惯猜。**
|
|
|
|
|
|
// 为什么要关:家书不从家谱图上改关系(都在各人资料页里改,那里有权限与校验)。
|
|
|
|
|
|
// 不关的话树上会多一张没有名字的空卡(data-id 是库现生成的 uuid、
|
|
|
|
|
|
// 内容为空、点了跳空页),还会因为排在已婚的「我」旁边而像"多出一个配偶"。
|
|
|
|
|
|
chart.editTree().setCanAdd(false);
|
|
|
|
|
|
|
|
|
|
|
|
// ⚠️ 关掉「单亲补空卡」。库默认(single_parent_empty_card: true)会在
|
|
|
|
|
|
// 「某个孩子只登记了一位父母」时,**自动给那位父母补一个配偶占位卡**,
|
|
|
|
|
|
// 好让孩子在图上"看着有两位父母"。实测:测试夹具里有个孩子**只登记了父亲**
|
|
|
|
|
|
// (没登记母亲),库里就凭空造了一张**没有名字、没有称谓、点了跳空页**的卡
|
|
|
|
|
|
// 挂在父亲旁边 —— 可在用户眼里这就是"我凭空多出一个配偶",比"缺一位家长"糟得多。
|
|
|
|
|
|
// 家书本来就有自己的占位语义(真实库记录 + 「待确认」角标),不需要库再补。
|
|
|
|
|
|
// ⚠️ 这个方法名在 chart 原型上(实测确认过),与 setCanAdd 不在同一处。
|
|
|
|
|
|
chart.setSingleParentEmptyCard(false);
|
|
|
|
|
|
|
|
|
|
|
|
// initial:true 才会画出第一版;fit 让整棵树自适应容器大小
|
|
|
|
|
|
chart.updateTree({ initial: true, tree_position: 'fit' });
|
|
|
|
|
|
window.kinChart = chart;
|
2026-10-02 09:28:07 +08:00
|
|
|
|
|
|
|
|
|
|
// ---- 平移 / 居中 ----
|
|
|
|
|
|
// 库自带拖拽与滚轮缩放,但**没有**"定位到某个人"的 API(method 清单里没有)。
|
|
|
|
|
|
// 好在缩放是标准 d3.zoom,它把对象挂在 svg 元素上(__zoomObj / __zoom),
|
|
|
|
|
|
// 直接拿它改 transform 就行 —— 不用碰库的内部状态。
|
|
|
|
|
|
// ⚠️ transform 的平移量是**屏幕像素**:想把某张卡挪到容器中心,
|
|
|
|
|
|
// 就是把它当前的屏幕中心与容器中心之差加到 t.x / t.y 上,k 不动。
|
|
|
|
|
|
var svg = root.querySelector('svg.main_svg');
|
|
|
|
|
|
|
|
|
|
|
|
function currentTransform() {
|
|
|
|
|
|
if (!svg) return null;
|
|
|
|
|
|
return svg.__zoom || null;
|
|
|
|
|
|
}
|
|
|
|
|
|
function applyTransform(t) {
|
|
|
|
|
|
if (!svg || !svg.__zoomObj || !window.d3) return;
|
|
|
|
|
|
window.d3.select(svg).call(svg.__zoomObj.transform, t);
|
|
|
|
|
|
}
|
|
|
|
|
|
function centerOn(id) {
|
|
|
|
|
|
var el = root.querySelector('.kin-card[data-kin-id="' + id + '"]');
|
|
|
|
|
|
if (!el || !window.d3) return;
|
|
|
|
|
|
var t = currentTransform();
|
|
|
|
|
|
if (!t) return;
|
|
|
|
|
|
var rr = root.getBoundingClientRect();
|
|
|
|
|
|
var er = el.getBoundingClientRect();
|
|
|
|
|
|
var dx = (rr.left + rr.width / 2) - (er.left + er.width / 2);
|
|
|
|
|
|
var dy = (rr.top + rr.height / 2) - (er.top + er.height / 2);
|
|
|
|
|
|
applyTransform(window.d3.zoomIdentity.translate(t.x + dx, t.y + dy).scale(t.k));
|
|
|
|
|
|
}
|
|
|
|
|
|
window.kinCenterOn = centerOn;
|
|
|
|
|
|
|
|
|
|
|
|
var meId = root.getAttribute('data-me') || '';
|
|
|
|
|
|
// 库是「过渡」出来的:卡片位置要等动画停了才准。等一小会儿再居中。
|
|
|
|
|
|
if (meId) { setTimeout(function () { centerOn(meId); }, 700); }
|
|
|
|
|
|
|
|
|
|
|
|
var btnFit = document.getElementById('kinFit');
|
|
|
|
|
|
var btnMe = document.getElementById('kinMe');
|
|
|
|
|
|
if (btnFit) {
|
|
|
|
|
|
btnFit.addEventListener('click', function () {
|
|
|
|
|
|
chart.updateTree({ tree_position: 'fit' });
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
if (btnMe && meId) {
|
|
|
|
|
|
btnMe.addEventListener('click', function () { centerOn(meId); });
|
|
|
|
|
|
}
|
2026-10-01 19:35:48 +08:00
|
|
|
|
})();
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
2026-09-30 08:13:07 +08:00
|
|
|
|
<?php endif; ?>
|