v24 第 2 轮:19 条清单收尾(附件预览/CalDAV/任务分类看板/全局选人/家谱/纪念日/财务)

用户 2026-10-02 提的 19 条需求,本轮完成剩下的 8 条 + CalDAV 服务端:

- ③ 附件:图片/视频给缩略图,PDF/Word/Excel 给**同尺寸**图标;可预览的在本页
     弹层里看(不再新开标签页),渲染不了的点了直接下载;预览层右上角恒有下载按钮。
- ⑦b CalDAV:把「我的日历本」按 RFC 4791 只读子集分享出去(OPTIONS/PROPFIND/
     REPORT/GET,写方法一律 403)。用**专用口令**(库里只存 sha256),不是登录口令。
- ⑨⑩ 任务:分类与看板都由用户自建;已完成的任务从看板消失,改在「已完成」页
     按分类查看/搜索;设置入口在「新建任务」旁的 ☰。
- ⑪ 全局人员选择器:一个片段铺到观影/旅程/资源/日历共享/任务共享/财务共享;
     修掉了老组件「名字和拼音都搜不动」的 bug —— .att-item 自己声明了 display:flex,
     压过了浏览器 [hidden]{display:none},过滤根本没生效。
- ⑫ 家谱:头像改方形不再裁切;姻亲不在图上的卡片给「⇄ 姻亲」入口,
     以 TA 为中心重画 + 「返回我的家谱」。
- ⑬ 纪念日:阴历/阳历都能设且按对应历法重复;名册里每个人的生日默认可见
     (实时派生,不复制一份);全部作为全天日程进日历。
- ⑮ 财务:账本可共享,双方都能记账、都能看全部账目,资产−负债=净资产;
     金额一律以「分」存,不用浮点。

验证:端到端 724 项通过 / 0 失败;几何 368、交互 97、移动端 516、家谱 24、
升级 123 全绿;SQL 列名静态校验 0 处可疑;部署 11 条线上探针全过。
This commit is contained in:
2026-10-03 08:18:05 +08:00
parent 72dc6c0006
commit 8f80f3e835
47 changed files with 5363 additions and 578 deletions
+79 -14
View File
@@ -20,12 +20,18 @@ $people = $people ?? [];
// familyMap 的 $up/$down 只影响**代数标号**(超出就压到边界那一档),
// 遍历本身是走遍整个连通图的 —— 所以这两个值不放宽的话,
// 曾祖辈与祖辈会被压成同一代,排序就乱了。给足余量。
$map = \App\Kinship::familyMap((int)\App\Auth::familyId(), (int)$meId, $people, 9, 9);
// ⚠️ 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);
$gen = $map['gen'];
$units = $map['units'];
$unitOf = $map['unit_of'];
if (!isset($people[$meId]) || !is_array($people[$meId])) {
if (!isset($people[$focusId]) || !is_array($people[$focusId])) {
$canChart = false;
} else {
$onTree = array_fill_keys(array_map('intval', array_keys($gen)), true);
@@ -37,7 +43,7 @@ if (!isset($people[$meId]) || !is_array($people[$meId])) {
// 把树根换成我的最高祖先,整棵树就变成「这位祖先的全部后代」,
// 上一代、这一代、下一代的血亲与旁支才能全部落进来。
// 代价是「我」不再是几何中心 —— 用「回到我」按钮 + 加载后自动居中补回来。
$rootId = (int)$meId;
$rootId = (int)$focusId;
for ($guard = 0; $guard < 8; $guard++) {
$up = \App\Kinship::parentIds($graph, $rootId);
$next = 0;
@@ -108,7 +114,9 @@ if (!isset($people[$meId]) || !is_array($people[$meId])) {
'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,
// 「我」在图上高亮;中心那个人另有标记($focusId)
'me' => $pid === (int)$focusId,
'is_me_viewer' => $pid === (int)$meId,
'ph' => (int)($p['is_placeholder'] ?? 0) === 1,
],
'rels' => [
@@ -119,6 +127,16 @@ if (!isset($people[$meId]) || !is_array($people[$meId])) {
];
}
// v24 第 12 条:本图上「有姻亲但没画出来」的人 —— 卡片上给个入口
$branchOuts = \App\Kinship::branchOuts($graph, $people, $onTree);
foreach ($datum as &$dn) {
$did = (int)$dn['id'];
if (isset($branchOuts[$did])) {
$dn['data']['branch'] = $branchOuts[$did]['name'];
}
}
unset($dn);
$canChart = count($datum) > 1;
}
@@ -130,8 +148,18 @@ $datumJson = json_encode(
?>
<div class="page-head">
<h1>我的家谱</h1>
<span class="sub">以我为中心 · 上下一共五代 · 夫妻并排,子女从两人之间的连线连出</span>
<h1><?= $focusId === (int)$meId ? '我的家谱' : e((string)($people[$focusId]['name'] ?? '') . ' 的家谱') ?></h1>
<span class="sub">
<?= $focusId === (int)$meId
? '以我为中心 · 夫妻并排,子女从两人之间的连线连出'
: '以 TA 为中心(这样 TA 那一侧的姻亲才露得出来)' ?>
</span>
<div class="actions">
<?php if ($focusId !== (int)$meId): ?>
<?php /* v24 第 12 条:从姻亲入口进来的,给一个返回,回到「我的家谱」 */ ?>
<a class="btn btn-ghost" href="<?= e(url('kin')) ?>">← 返回我的家谱</a>
<?php endif; ?>
</div>
</div>
<?php if (!$canChart): ?>
@@ -144,7 +172,8 @@ $datumJson = json_encode(
<div class="kin-tools">
<button type="button" class="btn btn-ghost btn-sm" id="kinFit">看全部</button>
<button type="button" class="btn btn-ghost btn-sm" id="kinMe">回到我</button>
<?php /* 中心是别人时这颗按钮是"跳回我的家谱"(不是在本图里找我的坐标) */ ?>
<button type="button" class="btn btn-ghost btn-sm" id="kinMe"><?= $focusId === (int)$meId ? '回到我' : '回到我的家谱' ?></button>
</div>
<!-- ⚠️ `f3` 这个类名是**库的 CSS 作用域**,不是我们起的名字。
@@ -152,7 +181,8 @@ $datumJson = json_encode(
少了它,库里 `.f3 svg.main_svg{width:100%;height:100%}` 这条撑满规则不生效,
SVG 会退化成浏览器默认的 300×150(连线全被裁掉,而 HTML 卡片层不受影响 →
表现为「卡在、线没了」)。 -->
<div class="kin-chart f3" id="kinChart" data-datum="<?= e($datumJson) ?>" data-me="<?= e((string)$meId) ?>"></div>
<div class="kin-chart f3" id="kinChart" data-datum="<?= e($datumJson) ?>"
data-focus="<?= e((string)$focusId) ?>" data-me-viewer="<?= e((string)$meId) ?>"></div>
<p class="tree-legend">
每代一行;<b>一对夫妻之间有一条短横线</b>,
@@ -197,22 +227,29 @@ $datumJson = json_encode(
// 会跳过深度截断,等于**不限代数**,有多少画多少。
// 之前写死了具体代数,真实家庭一超过就被砍掉。
// 用户要求「全部取出来」,所以这里也不设。
.setCardXSpacing(180)
.setCardXSpacing(190)
.setCardYSpacing(180);
chart.setCardHtml()
.setStyle('rect')
.setCardDim({ w: 140, h: 120, mini_tree: false })
.setCardDim({ w: 148, h: 138, mini_tree: false })
.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' : '');
// v24 第 12 条:头像改**方形**(原来是圆角矩形 + cover,把脸裁掉了)。
// 卡片本身是 <a> 进成员页;有姻亲没画出来的,再叠一个独立的小按钮
// (用 <span role=button> 而不是 <a>:<a> 不能套 <a>)。
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>'
+ '<span class="kin-lb">' + esc(p.label) + '</span>'
+ (p.ph ? '<span class="kin-mark">待确认</span>' : '')
+ (p.branch
? '<span class="kin-branch" role="button" tabindex="0" title="看看 '
+ esc(p.branch) + ' 那一侧的家谱" data-branch-id="' + esc(id) + '">⇄ 姻亲</span>'
: '')
+ '</a>';
});
@@ -256,12 +293,15 @@ $datumJson = json_encode(
}
window.kinCenterOn = centerOn;
var meId = root.getAttribute('data-me') || '';
var focusId = root.getAttribute('data-focus') || '';
var meId = root.getAttribute('data-me-viewer') || focusId;
// ⚠️ 中心是别人时,「回到我」要跳到 focus=我 的图,而不是在图里找我的坐标
var centerTarget = root.getAttribute('data-me-viewer') || focusId;
// fit 动画完成后把镜头挪到「我」(树根是最高祖先,我不再是几何中心)
if (meId) {
setTimeout(function () { centerOn(meId); }, 900);
setTimeout(function () { centerOn(focusId); }, 900);
// 万一 zoom 对象还没初始化好,再补一次
setTimeout(function () { centerOn(meId); }, 1400);
setTimeout(function () { centerOn(focusId); }, 1400);
}
var btnFit = document.getElementById('kinFit');
@@ -271,8 +311,33 @@ $datumJson = json_encode(
chart.updateTree({ tree_position: 'fit' });
});
}
/* v24 第 12 条:卡片右下角的「⇄ 姻亲」按钮 —— 以那个人为中心重画一张家谱。
⚠️ 这个 span 在卡片的 <a> 里,必须 preventDefault + stopPropagation,
否则会先触发外面那个链接跳去成员页。 */
Array.prototype.forEach.call(root.querySelectorAll('.kin-branch'), function (btn) {
function go(e) {
e.preventDefault();
e.stopPropagation();
var bid = btn.getAttribute('data-branch-id') || '';
if (bid) { window.location.href = '/index.php?r=kin&focus=' + encodeURIComponent(bid); }
}
btn.addEventListener('click', go);
btn.addEventListener('keydown', function (e) {
if (e.key === 'Enter' || e.key === ' ') { go(e); }
});
// 键盘/触摸都别让它冒泡到外层链接
btn.addEventListener('mousedown', function (e) { e.stopPropagation(); });
});
if (btnMe && meId) {
btnMe.addEventListener('click', function () { centerOn(meId); });
btnMe.addEventListener('click', function () {
// 图是别人的家谱时,先跳回「我的家谱」再居中
if (focusId !== meId) {
window.location.href = '/index.php?r=kin';
return;
}
centerOn(centerTarget);
});
}
})();
</script>