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
+150 -1
View File
@@ -246,6 +246,10 @@ final class Kinship
return ['gen' => [], 'units' => [], 'unit_of' => [], 'me' => $me];
}
// ⚠️ $up / $down 是**代数标号的上下限**(clamp),不是遍历深度 ——
// 走是走遍整个连通图的。所以同一个人可能通过两条路算出不同代数,
// 下面取「离我最近」的那一档。
// ---- ① 代数:以「我」为 0 代,反复贯穿直到不再有人可加 ----
$gen = [$me => 0];
$seen = [$me => true];
@@ -312,6 +316,145 @@ final class Kinship
return ['gen' => $gen, 'units' => $units, 'unit_of' => $unitOf, 'me' => $me];
}
/**
* v24 用户第 12 条:谁的姻亲「没画在家谱上」。
*
* 家谱画的是「同一位祖先的血脉后代 + 各人配偶」,所以**配偶那一侧的
* 父母/兄弟姐妹**天然不在图内(洪博为的父母不会出现在以杨家为根的树上)。
* 用户要的是:这些人的卡片上给一个入口,点一下就以 TA 为中心重画一张,
* 把 TA 那侧的姻亲露出来,再给个「返回」回到原图。
*
* 判据:**这个人有父母,但父母不在"库实际画出来的那批人"里** →
* TA 是有独立亲缘家族的人,点一下就以 TA 为中心重画。
*
* ⚠️ 这里的关键是"实际画出来的" ≠ "我们遍历到的":
* familyMap 为了算称呼会走遍**整个连通图**(连配偶的父母都拉进来),
* 但 family-chart 只画「根的祖先链 + 根的后代 + 根的兄弟姐妹 + 这些人的配偶」——
* **配偶的父母不在画布上**。拿遍历集合当判据,就会得出"岳父母已经在家谱里了"
* 这种与眼睛看到的相反的结论(实测踩到)。
*
* @param array<int,true> $onTree 遍历到的人(用于确定根与限制范围)
* @return array<int, array{name:string, count:int}> 人 id => 提示文案
*/
public static function branchOuts(array $g, array $people, array $onTree): array
{
// 本图的根 = 辈分最高的那个人(与 kin/tree 的取法一致)
$rootId = 0;
foreach (array_keys($onTree) as $id) {
$id = (int)$id;
if (!isset($people[$id])) {
continue;
}
$hasParentOnTree = false;
foreach (self::parentIds($g, $id) as $p) {
if (isset($onTree[(int)$p])) {
$hasParentOnTree = true;
break;
}
}
if (!$hasParentOnTree) {
$rootId = $id;
break;
}
}
if ($rootId === 0) {
return [];
}
$drawn = self::drawnOf($g, $rootId, $onTree);
$out = [];
foreach (array_keys($drawn) as $id) {
$id = (int)$id;
if ($id === $rootId || !isset($people[$id])) {
continue;
}
$parents = self::parentIds($g, $id);
if (!$parents) {
continue; // 不知道父母的(根、待确认的占位人)没什么可展开
}
$outside = 0;
foreach ($parents as $p) {
if (!isset($drawn[(int)$p])) {
$outside++;
}
}
if ($outside === 0) {
continue; // 父母都在画布上,这一侧已经露出来了
}
$out[$id] = [
'name' => (string)($people[$id]['name'] ?? ''),
'count' => $outside,
];
}
return $out;
}
/**
* family-chart 实际会画出来的人(近似复刻它的选择规则)。
*
* 规则(按库的行为):
* ① 根的**祖先链**(一直往上);
* ② 根的**后代**(一直往下 —— 库在未设上限时不截断);
* ③ 根的**兄弟姐妹**(与根共享父母的人);
* ④ 以上所有人的**配偶**(夫妻并排是库的基本单位)。
*
* ⚠️ **配偶的父母不在其中** —— 这正是"姻亲看不到"的根源,也是本函数存在的理由。
*
* @param array<int,true> $inScope 只在遍历到的人里挑(脏数据不越界)
* @return array<int,true>
*/
private static function drawnOf(array $g, int $rootId, array $inScope): array
{
$drawn = [];
$mark = static function (int $id) use (&$drawn, $inScope): bool {
if (!isset($inScope[$id]) || isset($drawn[$id])) {
return false;
}
$drawn[$id] = true;
return true;
};
$mark($rootId);
// ① 祖先链
$cur = $rootId;
for ($i = 0; $i < 12; $i++) {
$next = 0;
foreach (self::parentIds($g, $cur) as $p) {
if (isset($inScope[(int)$p])) { $next = (int)$p; break; }
}
if ($next === 0) { break; }
$mark($next);
$cur = $next;
}
// ③ 根的兄弟姐妹(共享父母)
foreach (self::parentIds($g, $rootId) as $p) {
foreach (self::childIds($g, (int)$p) as $sib) {
$mark((int)$sib);
}
}
// ② 后代:从根出发逐层往下;④ 每加进一个人,配偶跟着进来
$queue = array_keys($drawn);
while ($queue) {
$id = (int)array_shift($queue);
foreach (self::spouseIds($g, $id) as $s) {
if ($mark((int)$s)) { $queue[] = (int)$s; }
}
foreach (self::childIds($g, $id) as $c) {
if ($mark((int)$c)) { $queue[] = (int)$c; }
}
}
// 收尾再来一轮配偶(新进来的孩子要带上他们的配偶)
foreach (array_keys($drawn) as $id) {
foreach (self::spouseIds($g, (int)$id) as $s) {
$mark((int)$s);
}
}
return $drawn;
}
/**
* v15:把「孩子」按**父母所在的户**归堆。
*
@@ -566,7 +709,13 @@ final class Kinship
return $isKnown ? ($isMale ? '表兄弟' : '表姐妹') : '表亲';
}
return '亲属';
// ⚠️ v24(用户 2026-10-02 第 1 条):这里原来兜底返回「亲属」。
// 后果是名册里**每个没有血缘关系的人**(同事、朋友、老师…)都被贴上「亲属」——
// 人家明明只是个联系人,叫"亲属"是错的,而且人一多满屏都是同一个词,
// 看不出任何信息。「亲属」这个词本身也没有定义:关系推导不出来就是没关系,
// 硬套一个称呼反而是编造。
// 现在改为**返回空串**:没关系就不显示标签。调用方一律用 empty() 判断。
return '';
}
/** 配偶的称呼:按「我的性别」定,因为中文里丈夫/妻子是从自己视角说的 */