diff --git a/app/ApiHandler.php b/app/ApiHandler.php index 8ac423d..3b35a6c 100644 --- a/app/ApiHandler.php +++ b/app/ApiHandler.php @@ -224,6 +224,20 @@ final class ApiHandler error_log('[familybook] 登录取家庭名失败(已跳过):' . $ex->getMessage()); } + /* 🔴 2026-10-18:登录响应直接带上**我该显示的那句标语**。 + 用户要求把抽屉左上角那行(原来是「我的家」)换成标语: + 有个人 slogan 用个人的、没有用家庭的、再没有用应用默认口号 + (三层优先级见 `Family::slogan()`,与网页端首页同一个函数)。 + ⚠️ 放在登录里而不是只放 `/me`:登录后**立刻**就要渲染抽屉, + 少一次请求就少一次"先显示旧值再跳变"的闪烁。 */ + $slogan = Family::DEFAULT_SLOGAN; + try { + $slogan = Family::slogan($familyId, $personId); + } catch (\Throwable $ex) { + error_log('[familybook] 登录取标语失败(回退默认):' . $ex->getMessage()); + } + $avatarRaw = (string)($row['avatar'] ?? ''); + Api::ok([ 'token' => $token, // 到期时间由**服务端**算(端上只拿来显示,绝不据此判断"还能不能用") @@ -234,11 +248,16 @@ final class ApiHandler 'person' => [ 'id' => $personId, 'name' => (string)$row['name'], - 'avatar' => (string)($row['avatar'] ?? ''), + 'avatar' => $avatarRaw, + /* 🔴 头像 URL 与 `/me` 同口径(`avatar_url()` 会按线上存储驱动 + 决定走 /uploads 还是 OSS 直链)。 + ⚠️ 端上顶栏原来只拿色块字、拿不到真图 —— 用户 2026-10-18: + 「右上角的头像应该是真实的」。 */ + 'avatar_url'=> avatar_url($avatarRaw !== '' ? $avatarRaw : null), 'role' => (string)$row['role'], 'family_id' => $familyId, ], - 'family' => ['id' => $familyId, 'name' => $familyName], + 'family' => ['id' => $familyId, 'name' => $familyName, 'slogan' => $slogan], // 回显设备,端上可以核对"服务器记住的是不是这台" 'device' => [ 'id' => $deviceId, @@ -451,9 +470,22 @@ final class ApiHandler 'avatar' => avatar_url($p['avatar'] ?? null), 'role' => (string)($act['role'] ?? 'member'), 'can_write'=> (bool)($act['can_write'] ?? false), - 'family' => (function () use ($fam): array { + 'family' => (function () use ($fam, $pid): array { $f = Family::find($fam) ?: []; - return ['id' => $fam, 'name' => (string)($f['name'] ?? '')]; + /* 🔴 2026-10-18:标语一并给出来(抽屉左上角那行要它)。 + 每次启动都会打 `/me`,所以网页端改了 slogan 之后 + 手机下次启动就会跟上 —— 不必专门为它加一个接口。 */ + $sl = Family::DEFAULT_SLOGAN; + try { + $sl = Family::slogan($fam, $pid); + } catch (\Throwable $ex) { + // 读不到就用默认,不挡登录校验 + } + return [ + 'id' => $fam, + 'name' => (string)($f['name'] ?? ''), + 'slogan' => $sl, + ]; })(), // 各模块主题色:App 端顶部/分类色块直接用它,不必自己维护一张表 'colors' => $this->moduleColors($fam), @@ -647,6 +679,33 @@ final class ApiHandler // ---- 日子标记:节气 / 法定节日 / 传统节日 / 纪念日 / 生日 ---- $marks = Home::todayMarks($fam, $pid, $today); + /* ---- 🔴 v59(用户 2026-10-18):最近三个纪念日(含生日),倒计时用 ---- + 用户原话:「首页应该加一个『最近纪念日』模块,放在『往年的今天』前面、 + 『近期任务』后面。内容包括:1. 未来最近的三个纪念日,包括每个人的生日, + 以及手动填写的各个纪念日。2. 以倒计时形式展示:距离哪个纪念日还有多少天, + 距离谁谁谁的生日还有多少天。」 + ⇒ 位置是**近期任务之后、往年的今天之前**(端上按这个顺序摆)。 + + ⚠️ 为什么要 370 天的窗口(而不是像网页端概述卡那样只取 30 天): + 「未来最近的三个」如果窗口太小,一个刚过完生日的家庭会**一个都取不到**, + 模块直接空着 —— 而用户要的是一个**永远的倒计时**。 + 取满一年,保证"接下来的三个"一定有(只要有纪念日或生日)。 + ⚠️ 数据来源 `Anniversary::upcoming()` → `occurrences()` → `all()`, + 而 `all()` = **派生生日 + 自建纪念日** —— 正好一次满足用户说的两样, + 不必再去 `Birthday::` 那套单独取一遍(两处取必然口径不一)。 + ⚠️ 已经过完今天的同样包含在内(`from` 从今天起算,含今天)。 + 今天那条 `days=0` 端上会显示成「就是今天」。 */ + $recentDays = []; + try { + $recentDays = array_slice(Anniversary::upcoming($fam, $pid, 370), 0, 3); + } catch (\Throwable $ex) { + error_log('[familybook] 首页取最近纪念日失败(已跳过):' . $ex->getMessage()); + } + + // 当前登录者本人(顶栏右上角要显示**真实头像**,用户 2026-10-18: + // "右上角的头像应该是真实的")。取不到就留空串,端上回落成色块字。 + $meRow = Person::find($pid) ?: []; + // ---- 天气(三级回落:个人经纬度 → 内置城市表 → 家庭级兜底 → 不显示)---- $wloc = Home::weatherLocation($pid, $fam); $weather = $wloc === null @@ -700,9 +759,25 @@ final class ApiHandler * `kind` ∈ `movie` | `trip_node`(端上据此决定图标与圆点色)。 */ 'memories' => Home::memories($fam, 6)['rows'], + /** + * 🔴 v59:未来最近的三个纪念日(用户 2026-10-18 要的「最近纪念日」模块)。 + * + * 每条:`{id, date, title, kind, person_id, person_name?, source, days, age, nth}` + * · `days` —— 距今天还有几天(0 = 就是今天;**永远是 0 或正数**) + * · `kind` —— 用户填的类别("生日" / "结婚纪念" / …),端上直接显示 + * · `source`—— `'custom'` = 手动填的纪念日;其余(`birthday_solar` + * / `birthday_lunar`)= 派生生日,端上据此区分"纪念日 / 生日" + * · `title` —— 已含姓名(如「老杨 的生日」「结婚纪念日」),端上直接用 + * + * ⚠️ 顺序就是"最近的在前"(`upcoming` 内部已按 date 升序), + * 端上**不要再排一遍** —— 排序口径分散在两处迟早不一致。 + */ + 'recent_days' => $recentDays, 'today_events' => $todayEvents, 'tasks' => $tasks, 'unread' => Notification::unreadCount($pid), + /** 当前登录者的真实头像 URL(顶栏右上角要显示真图,用户 2026-10-18) */ + 'me_avatar' => avatar_url($meRow['avatar'] ?? null), ]); } diff --git a/app/Family.php b/app/Family.php index ef26e60..269fcf2 100644 --- a/app/Family.php +++ b/app/Family.php @@ -9,6 +9,65 @@ namespace App; final class Family { + /** + * 应用自己的口号(用户 2026-10-18 明确指定,别再改)。 + * + * 🔴 用户原话:「我们应用的口号或者标语是:**一家人用岁月写成的一本书**。 + * 这也是我们"家书"这个名字的由来。所以你不要再乱换这个标语了, + * 只要是想出现标语或者副标题的地方,就用这样一句话。」 + * + * ⚠️ 它与 `families.motto`(用户可在网页端「系统设置」里自己写一句) + * 是**两件事**:这里这句是"谁都没写时的默认值", + * 用户写了自己的就用他自己的 —— 规则见 `slogan()`。 + */ + public const DEFAULT_SLOGAN = '一家人用岁月写成的一本书'; + + /** + * 「我」这一端该显示的那句话(标语 / 副标题)。 + * + * 优先级(与网页端 `templates/home.php` 的 `heroSlogan` **同一套规则**): + * ① **我自己的** `persons.slogan`(在「我的资料」里填的那一句); + * ② 家庭的 `families.motto`(网页端「系统设置」里那句,用户可自定义); + * ③ 应用默认口号 `DEFAULT_SLOGAN`。 + * + * 🔴 为什么不是"简单取 motto":用户 2026-10-18 说得很清楚 —— + * 「如果这个用户自己有标语,他可以写一句自己的标语;……如果他没有标语, + * 就用我们给他的这个标语。」 + * 个人 → 家庭 → 默认,正是这三层;网页端首页的 `heroSlogan` 已经是这个逻辑, + * 端上照抄同一套,两边才不会出现"网页一句、手机另一句"。 + * + * @param int $familyId 家庭 id + * @param int|null $personId 当前登录者(null = 不取个人那句,直接走家庭/默认) + * @return string 永远非空(兜底是 DEFAULT_SLOGAN) + */ + public static function slogan(int $familyId, ?int $personId = null): string + { + if ($personId !== null && $personId > 0) { + try { + $st = Database::get()->prepare('SELECT slogan FROM persons WHERE id = ? AND family_id = ?'); + $st->execute([$personId, $familyId]); + $mine = trim((string)($st->fetchColumn() ?: '')); + if ($mine !== '') { + return $mine; + } + } catch (\Throwable $ex) { + // 老库还没这一列 / 读不到都不该挡路:往下走家庭那句 + } + } + + try { + $f = self::find($familyId); + $motto = trim((string)($f['motto'] ?? '')); + if ($motto !== '') { + return $motto; + } + } catch (\Throwable $ex) { + // 同上:读不到就用默认 + } + + return self::DEFAULT_SLOGAN; + } + public static function find(int $id): ?array { $st = Database::get()->prepare('SELECT * FROM families WHERE id = ?');