v14: 观影/家谱/日程/旅程/资源/任务六模块统一,首页改 dashboard
用户 14 条需求一次交付(1-13 功能 + 14 工作方式)。 观影(1-4) - 返回文案全模块统一为「返回观影」(不再有「返回列表」「返回观影记录」三种叫法) - 观影人一律用彩色人物标签 person_chip:底色由头像主色算出来(上传时存库), 瀑布流卡片也按人显示;五处观影模板全部改用它 - 人员显示统一走 person_name()(shown → 我给他起的称呼 → 姓名) - 影院管理页与年度报告页的上下两块用 .card + .card 统一给间距(原来紧贴成一整块) 家谱(5) - 树根从「我」换成**辈分最高的祖先**:库画的是"树根的祖先链+后代+树根的兄弟姐妹", 根=我 时伯父/堂兄/嫂子/侄子一个都不画(12 人夹具只画出 8 人) - focusMap 按**代数**圈定三代(不再按步数),normalizeLinks 把子女对称挂到父母双方 —— 修掉"母亲登录看不到孩子" - 新增「看全部 / 回到我」两个按钮,放在画布外(画布整块被库接管了手势) 统一事件原语(6-9) - 旅程:TripLog 全量迁入(3 趟旅程 / 21 段行程 / 72 张照片 / 5 个人)。 复用 events(kind='trip') + trip_details + trip_nodes(计划/执行双轨保留) - 日程:每人可建日历本(自定义色)+ 共享只读/可写;观影与旅行是每人默认两本, 内容是**从各模块实时派生**的(不复制,所以改了观影日期日历立刻跟着变); 没有具体时分 = 全天;月视图参照群晖 Calendar - 资源:挂在某人名下的附属信息(标题+正文+多附件),可共享给多人并设只读/读写, 默认折叠、可搜标题与正文 - 任务:看板三列 + 两级子任务 + 评论@人 + 共享 + 循环(完成时按原截止日推下一期) - 统一附件表:四个模块的附件共用一张,下载地址不落库、一律由 media.php 现算 模块化(10-11) - App\Module 是模块的唯一来源(加一行 = 导航条/主题色/个人设置三处自动出现) - 个人设置里可勾选要显示哪些模块、拖动排序(关掉的模块**不会**被兜底补回来) - 每个模块一套主题色(家庭级),只覆盖 --brand 三兄弟,不碰纸墨底色 其他(12-13) - 新增成员表单可选所属分组(编辑表单没有:分组是"我和他的关系",只归我自己) - 首页改 dashboard:接下来的日子 / 未来七天 / 手头在做 / 往年今日。 家人卡片从首页撤走(看人去名册) 验证(全部通过) - e2e 542 条(新增 4 模块端到端 60+ 条) - 桌面几何 368 / 移动端几何 520 / 家谱 12 / 交互 97 / 老库升级 123 - SQL 列名静态校验 0 处可疑(对着真实表结构) - 反向验证 18 条新注入全部抓住 顺带修掉的既有问题 - 老库升级回填 SQL 按实际存在的列动态拼(缺列不再 1054 炸断升级) - 全新安装时 Schema::ensure 跑在"人还没建"之前 → 补建日历本三处齐备 - 窄屏导航横向溢出(模块从 4 个加到 8 个后顶栏撑破视口)
This commit is contained in:
+146
-31
@@ -34,41 +34,102 @@
|
||||
$nodes = $tree['nodes'];
|
||||
$me = $nodes[$meId] ?? null;
|
||||
|
||||
// ---- 转成 family-chart 需要的 Datum 格式 ----
|
||||
// ⚠️ 「我」排最前(见上面坑①)。其余按名册顺序跟上,库自己会按辈分重新摆放。
|
||||
$order = [(int)$meId];
|
||||
foreach (array_keys($people) as $pid) {
|
||||
if ((int)$pid !== (int)$meId) {
|
||||
$order[] = (int)$pid;
|
||||
// ---- 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;
|
||||
}
|
||||
}
|
||||
if ($next === 0) {
|
||||
break;
|
||||
}
|
||||
$rootId = $next;
|
||||
}
|
||||
|
||||
$datum = [];
|
||||
// ---- 连边:子女一律挂在「夫妻」上(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 = [];
|
||||
foreach ($order as $pid) {
|
||||
if (!isset($people[$pid]) || !is_array($people[$pid])) {
|
||||
continue;
|
||||
}
|
||||
$p = $people[$pid];
|
||||
|
||||
// 只连「名册里真的有的人」——树上出现一个查不到的人会画出空白卡
|
||||
$parents = [];
|
||||
foreach (\App\Kinship::parents($graph, $pid) as $pp) {
|
||||
if ($pp !== null && isset($people[$pp])) {
|
||||
$parents[] = (string)$pp;
|
||||
$mapIds = static function (array $ids) use ($onTree): array {
|
||||
$out = [];
|
||||
foreach ($ids as $x) {
|
||||
$x = (int)$x;
|
||||
if (isset($onTree[$x])) {
|
||||
$out[] = (string)$x;
|
||||
}
|
||||
}
|
||||
}
|
||||
$spouses = [];
|
||||
foreach (\App\Kinship::spouseIds($graph, $pid) as $sp) {
|
||||
if (isset($people[$sp])) {
|
||||
$spouses[] = (string)$sp;
|
||||
}
|
||||
}
|
||||
$kids = [];
|
||||
foreach (\App\Kinship::childIds($graph, $pid) as $kd) {
|
||||
if (isset($people[$kd])) {
|
||||
$kids[] = (string)$kd;
|
||||
}
|
||||
}
|
||||
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);
|
||||
|
||||
// 库认 M / F / U 三种(决定卡片底色)
|
||||
$g = strtolower((string)($p['gender'] ?? ''));
|
||||
@@ -78,7 +139,7 @@ foreach ($order as $pid) {
|
||||
'id' => (string)$pid,
|
||||
'data' => [
|
||||
'gender' => $gender,
|
||||
'name' => (string)($p['name'] ?? ''),
|
||||
'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,
|
||||
@@ -106,7 +167,7 @@ $canChart = count($datum) > 1;
|
||||
|
||||
<div class="page-head">
|
||||
<h1>我的家谱</h1>
|
||||
<span class="sub">以我为中心 · 同代按长幼从左到右 · 夫妻并排 · 折线表示谁是谁的父母</span>
|
||||
<span class="sub">以我为中心 · 上一代 / 这一代 / 下一代 · 夫妻并排,子女从两人之间的横线连出</span>
|
||||
</div>
|
||||
|
||||
<?php if (!$canChart): ?>
|
||||
@@ -123,12 +184,22 @@ $canChart = count($datum) > 1;
|
||||
⚠️ 库初始化那一刻**容器必须已经有真实尺寸**:它按容器宽高算 fit 缩放,
|
||||
还在 0×0 时初始化会一路缩到 0.4 倍,卡片小到看不清(实测踩过)。 */
|
||||
?>
|
||||
<div class="kin-chart" id="kinChart" data-datum="<?= e($datumJson) ?>"></div>
|
||||
<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>
|
||||
|
||||
<p class="tree-legend">
|
||||
画的是**以我的最高祖先为根、本家血脉的整棵后代树**:父母与伯叔姑舅姨(上一代)、
|
||||
我与兄弟姐妹和堂表亲及各自的配偶(这一代)、他们的孩子(下一代)都在上面。
|
||||
「待确认」= 只填了称呼、还没对应到真实的人(虚线卡片)。
|
||||
图里画的是**本家血脉 + 各自的配偶**;配偶的父母(如岳父母)属姻亲,不在这张图上。
|
||||
可以直接**拖拽平移**、**滚轮缩放**;点一下卡片进对应成员页。
|
||||
夫妻并排,**子女一律从两人之间的横线连出** —— 不管是谁登录,孩子都挂在这对夫妻上。
|
||||
可以**拖拽平移**、**滚轮缩放**;点「回到我」把镜头挪回你自己,点卡片进对应成员页。
|
||||
</p>
|
||||
|
||||
<script src="<?= e(asset('/assets/vendor/family-chart/d3.min.js')) ?>"></script>
|
||||
@@ -173,7 +244,7 @@ $canChart = count($datum) > 1;
|
||||
// (抛 TypeError),异常把后面的 updateTree() 一起带走了 →
|
||||
// 整张家谱**一张卡都不画**(页面 200、容器有尺寸、看着"正常")。
|
||||
// 用 <a> 件不依赖任何库 API,也不怕库版本变了改签名。
|
||||
return '<a class="' + cls + '" href="/index.php?r=people/show&id='
|
||||
return '<a class="' + cls + '" data-kin-id="' + esc(id) + '" href="/index.php?r=people/show&id='
|
||||
+ encodeURIComponent(id) + '">'
|
||||
+ '<span class="kin-ph"><img src="' + esc(p.avatar) + '" alt=""></span>'
|
||||
+ '<span class="kin-nm">' + esc(p.name) + '</span>'
|
||||
@@ -207,6 +278,50 @@ $canChart = count($datum) > 1;
|
||||
// initial:true 才会画出第一版;fit 让整棵树自适应容器大小
|
||||
chart.updateTree({ initial: true, tree_position: 'fit' });
|
||||
window.kinChart = chart;
|
||||
|
||||
// ---- 平移 / 居中 ----
|
||||
// 库自带拖拽与滚轮缩放,但**没有**"定位到某个人"的 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); });
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user