2026-10-11 22:07:48 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 家书 FamilyBook · 首页(鸿蒙端)
|
|
|
|
|
|
*
|
2026-10-18 03:35:39 +08:00
|
|
|
|
* 对应 Web 端 `templates/home.php`。版面:
|
|
|
|
|
|
* ① 个人信息卡(问候 + 姓名 + 日期/星期/农历 + 日子标记 + 天气 + 近七天数字)
|
|
|
|
|
|
* ② 今天的日程(🔴 **直接用日程页那块面板组件** —— 用户 2026-10-16)
|
|
|
|
|
|
* ③ 近期任务(进行中 + 待办,进行中在前)
|
|
|
|
|
|
* ④ 往年的今天(用户 2026-10-17 要求加,与电脑端同一个逻辑)
|
2026-10-11 22:07:48 +08:00
|
|
|
|
*
|
2026-10-18 03:35:39 +08:00
|
|
|
|
* 🔴🔴 v54(用户 2026-10-16)—— 第一轮修的四件事:
|
|
|
|
|
|
* 1. 「概述块**太高了**……其实是因为我们调整之后,把家书那条调到上面去了,
|
|
|
|
|
|
* 所以概述块变得特别高。」
|
|
|
|
|
|
* 根因:`.padding({ bottom: FB_DOCK_RESERVE })`(**96vp** 的底部悬浮栏预留)
|
|
|
|
|
|
* 被接在了「天气地点 + 日子标记」那个小 Flex 上(缩进看着像它的修饰)——
|
|
|
|
|
|
* 它本该在**页面根部**(给最后一屏留白)。
|
|
|
|
|
|
* ⇒ 概述卡中间凭空多出 96vp 的空白,卡片被撑得老高。
|
|
|
|
|
|
* **修法:挪回页面根部的 Column。**
|
|
|
|
|
|
* 2. 「里面的数据好像有些不符,比如**寒露、重阳节、儿子的生日**,
|
|
|
|
|
|
* 这些都是**乱写的**,要用**真实数据**填写一下。」
|
|
|
|
|
|
* 根因:农历 / 日子标记 / 天气 / 周统计这四样接口**从来没给过**
|
|
|
|
|
|
* (网页版是在 index.php 里算好直接渲染的)⇒ 端上拿 `DemoData` 演示常量顶着。
|
|
|
|
|
|
* ⇒ 服务端 `/home` 已补齐(见 `ApiHandler::home()`),这里全部改接真实数据。
|
|
|
|
|
|
* 3. 「**今天的日程**……把今天那个块拿到首页上来就行了。」
|
|
|
|
|
|
* ⇒ 复用 `FbDayEventPanel`(日程页月/周/列表三处用的同一个组件),
|
|
|
|
|
|
* 于是时间轴/列表显示方式、冲突并排、当前时间红线在首页**与日程页完全一致**。
|
|
|
|
|
|
* 4. 「**我的家人、未来7天,还有全部模块**,这几个就是去掉了,不要了。」
|
|
|
|
|
|
* ⇒ 三个区块连同它们的演示常量一起删掉。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 🔴🔴 v54b(用户 2026-10-17)—— 第二轮:
|
|
|
|
|
|
* 5. 「概述卡……**日程、在做、待办的数量是错误的**。」
|
|
|
|
|
|
* 真机实测:接口与端上缓存都是 `{1,3,2,0}`(正确),界面却是 `0/0/0`。
|
|
|
|
|
|
* 根因:`FbStatTriple` 内部 `@Builder cell(label, value, alarm)` 是**多值参数**
|
|
|
|
|
|
* ⇒ SDK 规则「两个及以上参数不触发刷新」⇒ 永远停在初始的 0。
|
|
|
|
|
|
* ⇒ 抽 `FbStatCell` **组件**(@Param 参与观察)—— 本项目第 6 次同一类修法。
|
|
|
|
|
|
* 6. 「近期任务的下面,应该加上**往年的今天**,跟电脑端一样。」
|
|
|
|
|
|
* ⇒ 服务端把 `home_memories()` 搬成 `Home::memories()`(两端同一份实现),
|
|
|
|
|
|
* `/home` 给 `memories` 字段,这里加第 ④ 块。
|
2026-10-11 22:07:48 +08:00
|
|
|
|
*/
|
|
|
|
|
|
|
2026-10-21 20:01:37 +08:00
|
|
|
|
import { FbColors, FbMetric, FB_MASK } from '../../common/Theme';
|
2026-10-11 22:07:48 +08:00
|
|
|
|
import { FbAppState, fbAppState } from '../../common/Store';
|
2026-10-24 19:14:18 +08:00
|
|
|
|
import { FbCalEvent, FbCalBook, FbCatOpt, FbHomeData, FbMark, FbMemoryItem, FbRecentDay,
|
|
|
|
|
|
FbTaskItem, FbBoardOpt, FbTaskDetail, FbTaskMeta, taskKey,
|
2026-10-29 20:39:52 +08:00
|
|
|
|
FbWeather, isEditableEvent, isEventWritable } from '../../model/Types';
|
2026-10-21 20:01:37 +08:00
|
|
|
|
import { common } from '@kit.AbilityKit';
|
|
|
|
|
|
import { BusinessError } from '@kit.BasicServicesKit';
|
|
|
|
|
|
import { hilog } from '@kit.PerformanceAnalysisKit';
|
2026-10-18 03:35:39 +08:00
|
|
|
|
import { parseHome } from '../../data/FbParse';
|
2026-10-22 16:40:44 +08:00
|
|
|
|
import { loadModule, loadPath } from '../../data/FbModuleLoad';
|
2026-10-18 03:35:39 +08:00
|
|
|
|
import { FB_CAL_SETTINGS, FbCalStyle } from '../../data/FbCalSettings';
|
2026-10-21 20:01:37 +08:00
|
|
|
|
import { FbCalRepo, FbCalEventFull } from '../../data/FbCalRepo';
|
|
|
|
|
|
import { FB_TOKEN_STORE } from '../../data/FbTokenStore';
|
2026-10-22 16:40:44 +08:00
|
|
|
|
import { parseTaskMeta } from '../../data/FbParse';
|
2026-10-21 20:01:37 +08:00
|
|
|
|
import { FB_CARD_FORMS } from '../../data/FbCardForms';
|
|
|
|
|
|
import { fbApi, FbApiResult } from '../../data/FbHttp';
|
2026-10-18 03:35:39 +08:00
|
|
|
|
import { FbSectionTitle } from '../../components/Widgets';
|
|
|
|
|
|
import { FbStatCell } from '../../components/FbStatCell';
|
|
|
|
|
|
import { FbDayEventPanel } from '../../components/FbDayEventPanel';
|
2026-10-21 20:01:37 +08:00
|
|
|
|
import { FbEventSheet } from '../../components/FbEventSheet';
|
2026-10-24 19:14:18 +08:00
|
|
|
|
import { FbEventEditSheet } from '../../components/FbEventEditSheet';
|
2026-10-22 16:40:44 +08:00
|
|
|
|
import { FbTaskPanel } from '../../components/FbTaskPanel';
|
|
|
|
|
|
import { FbTaskEditSheet } from '../../components/FbTaskEditSheet';
|
2026-10-24 20:22:35 +08:00
|
|
|
|
/* v86:三张卡的新建表单 —— 全是**模块页正在用的同一个组件**,不另写。 */
|
|
|
|
|
|
import { FbDayEditSheet } from '../../components/FbDayEditSheet';
|
2026-11-05 13:43:03 +08:00
|
|
|
|
import { FbPersonSheet } from '../../components/FbPersonSheet';
|
|
|
|
|
|
import { FbSheetRing } from '../../components/FbSheetRing';
|
2026-10-24 20:22:35 +08:00
|
|
|
|
import { FbMovieNewSheet } from '../../components/FbMovieNewSheet';
|
|
|
|
|
|
import { FbPersonEditSheet } from '../../components/FbPersonEditSheet';
|
2026-11-05 13:43:03 +08:00
|
|
|
|
import { FbMovieRepo, FbMovieListItem, FbCinemaItem, FbMovieDetail } from '../../data/FbMovieRepo';
|
|
|
|
|
|
import { FbMovieDetailSheet } from '../../components/FbMovieDetailSheet';
|
2026-10-24 20:22:35 +08:00
|
|
|
|
import { FbPersonRepo, FbRosterPerson } from '../../data/FbPersonRepo';
|
2026-10-12 12:06:00 +08:00
|
|
|
|
import { FB_DOCK_RESERVE } from '../../components/FloatDock';
|
2026-10-18 03:35:39 +08:00
|
|
|
|
import { fbOnScroll, fbOnTouch } from '../../common/FbScroll';
|
2026-10-21 20:01:37 +08:00
|
|
|
|
import { fbMaskTransition, fbSheetTransition } from '../../common/FbAnim';
|
|
|
|
|
|
import { FB_BACK } from '../../common/FbBack';
|
|
|
|
|
|
import { fbOpenNavigation } from '../../common/FbNav';
|
2026-11-05 12:41:04 +08:00
|
|
|
|
import { FbDiag } from '../../common/FbDiag';
|
2026-10-21 20:01:37 +08:00
|
|
|
|
import { fbModuleOf, FbModuleDef, fbThemeVars } from '../../model/Modules';
|
2026-10-11 22:07:48 +08:00
|
|
|
|
|
2026-10-24 19:14:18 +08:00
|
|
|
|
/** 日志域(与 `FbBackgroundSync` / `FbNav` 同一个值,端上统一 0x0000) */
|
|
|
|
|
|
const DOMAIN: number = 0x0000;
|
|
|
|
|
|
const TAG: string = 'FbHomePage';
|
|
|
|
|
|
|
2026-10-11 22:07:48 +08:00
|
|
|
|
@ComponentV2
|
|
|
|
|
|
export struct FbHomePage {
|
2026-10-12 10:06:55 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 顶栏插槽:由 Index.ets 传入,放在本页滚动流最前面 ⇒ 随页面一起滚走。
|
|
|
|
|
|
* 🔴 用户要求「向上滑动时标题栏跟着一起滑走」。
|
|
|
|
|
|
*/
|
|
|
|
|
|
@BuilderParam topBar: () => void;
|
2026-10-11 22:07:48 +08:00
|
|
|
|
@Param app: FbAppState = fbAppState();
|
|
|
|
|
|
|
2026-10-18 03:35:39 +08:00
|
|
|
|
// ---------------- 数据(全部来自 `/api/v1/home`)----------------
|
|
|
|
|
|
/** 今天的日程(服务端已排好:全天在前、其余按时间) */
|
|
|
|
|
|
@Local todayEvents: FbCalEvent[] = [];
|
|
|
|
|
|
/** 近期任务(进行中在前、待办在后) */
|
|
|
|
|
|
@Local tasks: FbTaskItem[] = [];
|
|
|
|
|
|
/** 首页问候语 */
|
|
|
|
|
|
@Local greeting: string = '';
|
|
|
|
|
|
/** 抬头日期(`10 月 16 日`) */
|
|
|
|
|
|
@Local todayLabel: string = '';
|
|
|
|
|
|
/** 星期几(`星期五`) */
|
|
|
|
|
|
@Local weekday: string = '';
|
|
|
|
|
|
/** 农历短写(`八月廿二`) */
|
|
|
|
|
|
@Local lunarShort: string = '';
|
|
|
|
|
|
/** 日子标记(节气/节日/纪念日/生日) */
|
|
|
|
|
|
@Local marks: FbMark[] = [];
|
|
|
|
|
|
/** 天气(null = 没配地点 ⇒ 整块不画) */
|
|
|
|
|
|
@Local weather: FbWeather | null = null;
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 概述卡的四个数字。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 🔴 v54b:**拆成四个独立的 `@Local number`**,而不是一个 `@Local stat: FbStat`
|
|
|
|
|
|
* 对象。理由(本次真机 bug 的教训):数字是最可靠被观察的类型;
|
|
|
|
|
|
* 用对象时,刷新链路要经过"对象整体替换 → 子组件 @Param 收到新值"两道,
|
|
|
|
|
|
* 任何一道出问题都表现为"界面永远停在初始的 0"。
|
|
|
|
|
|
* ⚠️ 拆开之后,四个格子各自只依赖一个 number,链路最短。
|
|
|
|
|
|
*/
|
|
|
|
|
|
@Local statToday: number = 0;
|
|
|
|
|
|
@Local statDoing: number = 0;
|
|
|
|
|
|
@Local statTodo: number = 0;
|
|
|
|
|
|
@Local statOverdue: number = 0;
|
|
|
|
|
|
/** 往年的今天(`/home` 的 memories) */
|
|
|
|
|
|
@Local memories: FbMemoryItem[] = [];
|
2026-10-18 20:14:40 +08:00
|
|
|
|
/** 🔴 v59:最近三个纪念日(`/home` 的 recent_days,服务端已排好序) */
|
|
|
|
|
|
@Local recentDays: FbRecentDay[] = [];
|
2026-10-18 03:35:39 +08:00
|
|
|
|
/** 顶部提示(同步失败等) */
|
|
|
|
|
|
@Local syncNote: string = '';
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 日程显示方式(时间轴 / 列表)—— 与日程页**读同一个设置**。
|
|
|
|
|
|
* ⚠️ 首页显示"今天的日程",用户在哪一处把显示方式调了,另一处也该跟着变
|
|
|
|
|
|
* (它们本来就是同一份数据的不同入口)。
|
|
|
|
|
|
*/
|
|
|
|
|
|
@Local calStyle: FbCalStyle = 'timeline';
|
|
|
|
|
|
|
2026-10-24 19:14:18 +08:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// v84(用户 2026-10-23):首页卡片上的「新建 / 新增」按钮
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
/* 🔴 用户原话:「日程,没有新建日程按钮,名册,也没有新增成员这个按钮。」
|
|
|
|
|
|
------------------------------------------------------------------
|
|
|
|
|
|
⚠️ 这条**上一轮只做到 Web 端**(`app/HomeCards.php` 的 `TYPES['agenda']['add']`),
|
|
|
|
|
|
端上首页压根没做 —— 所以用户装完机仍然说"没有按钮"。
|
|
|
|
|
|
教训:这类"两端都要"的需求,改完一端必须**立刻去另一端 grep 同一件事**。
|
|
|
|
|
|
|
|
|
|
|
|
⚠️ 为什么首页要**自己拉一次 `/calendar/books`**、不复用日程页那份:
|
|
|
|
|
|
首页和日程页是两个独立组件(首页不持有日程页的 `@Local books`)。
|
|
|
|
|
|
把它塞进 `/home` 接口当然更省,但那要动服务端 + 端上解析 + 网页端,
|
|
|
|
|
|
收益只有"少一个请求",风险却横跨三层。
|
|
|
|
|
|
⇒ 折中:**按需懒加载**(点开新建才拉),且拉到后缓存在 `calBooks` 里,
|
|
|
|
|
|
同一会话内只拉一次。 */
|
|
|
|
|
|
@Local calBooks: FbCalBook[] = [];
|
|
|
|
|
|
/** 首页「新建日程」表单开着 */
|
|
|
|
|
|
@Local calCreating: boolean = false;
|
2026-10-24 20:22:35 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* v86:首页「编辑日程」表单开着(从详情卡点「编辑」进来)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 与 `calCreating` **必须分开**:两者渲染的是同一个 `FbEventEditSheet`,
|
|
|
|
|
|
* 但入参完全不同(`event: null` + `isCreate: true` + `presetDate`
|
|
|
|
|
|
* vs `event: evtFull` + `isCreate: false`)。合成一个布尔再在里面分叉,
|
|
|
|
|
|
* 会让"到底是新建还是编辑"取决于 `evtFull` 是否为 null —— 那是隐式契约,
|
|
|
|
|
|
* 改一处就静默错。分开两个状态位,一眼能看懂,`backOnce` 也好按层关。
|
|
|
|
|
|
*/
|
|
|
|
|
|
@Local calEditing: boolean = false;
|
2026-10-24 19:14:18 +08:00
|
|
|
|
/** 正在拉日历本(防连点开两个空表单) */
|
|
|
|
|
|
@Local calLoadingBooks: boolean = false;
|
|
|
|
|
|
|
2026-10-21 20:01:37 +08:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// v73(用户 2026-10-21):首页点日程 / 点任务 → **就地弹详情卡**
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
/* 🔴 用户原话:
|
|
|
|
|
|
「应用里面啊,就是首页上的那个日程和这个任务,点击之后都应该像那个日程和任务
|
|
|
|
|
|
里面的卡片一样弹出来,不要进入到日程里面。比如说一个日程,我在首页上点击之后
|
|
|
|
|
|
就能弹出和日程里面的卡片是一样的,看它里面的详情啊。呃,如果是有这个地址的,
|
|
|
|
|
|
还可以直接一键导航。任务也是一样,点击任务之后弹出来详情,可以标记可以标记
|
|
|
|
|
|
它是不是完成了什么之类的。」
|
|
|
|
|
|
⇒ 下面这八组状态就是这件事的全部:日程详情卡 / 任务详情卡 / 各自的开关。
|
|
|
|
|
|
⚠️ **不要**改回"点一下 `switchModule('calendar')`"—— 那是用户明确否掉的旧行为
|
|
|
|
|
|
(原来 `onPick` 只有一行 `this.app.switchModule('calendar')`,
|
|
|
|
|
|
点哪一条都跳到日程页,用户还得自己再找一遍那条)。 */
|
|
|
|
|
|
|
|
|
|
|
|
/** 摘要卡显示的那条日程(列表那份;非 null = 日程详情卡开着) */
|
|
|
|
|
|
@Local evtBase: FbCalEvent | null = null;
|
|
|
|
|
|
/** 日程详情(`/events/<id>` 的完整字段;null = 还在拉 / 拉不到) */
|
|
|
|
|
|
@Local evtFull: FbCalEventFull | null = null;
|
|
|
|
|
|
/** 日程详情加载中 */
|
|
|
|
|
|
@Local evtLoading: boolean = false;
|
|
|
|
|
|
/** 日程详情加载失败说明(空 = 没失败) */
|
|
|
|
|
|
@Local evtError: string = '';
|
|
|
|
|
|
/** 丢弃在途请求的令牌(连点两条日程时,先回来的那条不该覆盖后点的) */
|
|
|
|
|
|
@Local evtToken: number = 0;
|
|
|
|
|
|
|
2026-10-22 16:40:44 +08:00
|
|
|
|
/** 摘要卡显示的那条任务(非 null = 任务面板开着) */
|
2026-10-21 20:01:37 +08:00
|
|
|
|
@Local tskBase: FbTaskItem | null = null;
|
2026-11-05 13:43:03 +08:00
|
|
|
|
|
|
|
|
|
|
/* ============================================================
|
|
|
|
|
|
#62/#63/#64/#65:首页各区块的**只读详情卡**(先看后改)
|
|
|
|
|
|
============================================================
|
|
|
|
|
|
🔴 用户 2026-11-05:
|
|
|
|
|
|
「近期任务、纪念日、最近观影都像日程一样,弹出来一个卡片,
|
|
|
|
|
|
点编辑的时候才进入编辑。」
|
|
|
|
|
|
「最近观影点了之后没反应…点了之后应该有反应,要么弹出来详细信息。」
|
|
|
|
|
|
「首页上的联系人…点击之后弹出来详细信息,就跟名册里面那个人的列表
|
|
|
|
|
|
点击完之后的效果是一样的。」
|
|
|
|
|
|
「往年的今天也是一样,点完其中一个之后…就应该弹出来跟电影有关的弹框卡片。」
|
|
|
|
|
|
|
|
|
|
|
|
⚠️ 这些字段都要计入 `sheetCount`(见 @Monitor)——
|
|
|
|
|
|
否则按返回键会直接退出页面,而不是先关掉详情卡。 */
|
|
|
|
|
|
/** 最近观影的详情(null = 没开) */
|
|
|
|
|
|
@Local mvDetail: FbMovieDetail | null = null;
|
|
|
|
|
|
/** 观影详情正在拉 */
|
|
|
|
|
|
@Local mvDetailLoading: boolean = false;
|
|
|
|
|
|
/** 观影详情拉取失败说明 */
|
|
|
|
|
|
@Local mvDetailError: string = '';
|
|
|
|
|
|
/** 被点的观影条目(列表自带评分/短评数,详情还没到时先用它垫) */
|
|
|
|
|
|
@Local mvSeed: FbMovieListItem | null = null;
|
|
|
|
|
|
|
|
|
|
|
|
/** 联系人详情(null = 没开) */
|
|
|
|
|
|
@Local pfDetail: FbRosterPerson | null = null;
|
|
|
|
|
|
|
|
|
|
|
|
/** 纪念日详情(null = 没开) */
|
|
|
|
|
|
@Local dayDetail: FbRecentDay | null = null;
|
|
|
|
|
|
|
|
|
|
|
|
/** 往年的今天 —— 被点的条目(按 kind 分发卡片) */
|
|
|
|
|
|
@Local memDetail: FbMemoryItem | null = null;
|
|
|
|
|
|
|
|
|
|
|
|
/* ============================================================
|
|
|
|
|
|
#59:概述四格数字 → 锚点跳转
|
|
|
|
|
|
============================================================
|
|
|
|
|
|
🔴 用户 2026-11-05:「点击上面这几个数字的时候,要滑到首页上的
|
|
|
|
|
|
对应位置,也就是锚点处…它是有作用的,不只是一个数字。」
|
|
|
|
|
|
|
|
|
|
|
|
⚠️ ArkUI 没有 Web 的 `#anchor`,用 `Scroller.scrollTo` 量坐标实现。
|
|
|
|
|
|
⚠️ 首页是 `Scroll + Column`(不是 List),没有"项索引"可滚,
|
|
|
|
|
|
所以只能记每个区块的 y 偏移。
|
|
|
|
|
|
⚠️ 用 `@Local` 而不是普通字段:坐标是 `onAreaChange` 进来的,
|
|
|
|
|
|
虽然只在点击时读一次,但保持与其它状态一致,避免有人在
|
|
|
|
|
|
`build` 里读它却拿不到最新值。 */
|
|
|
|
|
|
private homeScroller: Scroller = new Scroller();
|
|
|
|
|
|
/** 「今天的日程」区块在滚动内容里的 y 偏移 */
|
|
|
|
|
|
@Local yEvents: number = 0;
|
|
|
|
|
|
/** 「近期任务」区块的 y 偏移 */
|
|
|
|
|
|
@Local yTasks: number = 0;
|
2026-10-22 16:40:44 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* v79:面板里能用的**全部**看板 / 分类(`?view=boards`)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 🔴 为什么不在页面加载时就拉:首页 `/home` 不返回看板表,
|
|
|
|
|
|
* 而"挪分组/选分类"只有点开任务后才用得上 —— 按需拉更省一次请求。
|
|
|
|
|
|
* ⚠️ 拉的是**全部看板**(含空看板),不是从任务里反推
|
|
|
|
|
|
* (反推会让新建的空看板永远选不到)。
|
|
|
|
|
|
*/
|
|
|
|
|
|
@Local tskBoards: FbBoardOpt[] = [];
|
|
|
|
|
|
@Local tskCats: FbCatOpt[] = [];
|
|
|
|
|
|
/** 任务编辑表单是否打开(v79:在首页直接改,不再跳任务页) */
|
|
|
|
|
|
@Local tskEditOpen: boolean = false;
|
|
|
|
|
|
/** 编辑表单回填的字段 */
|
|
|
|
|
|
@Local tskEditId: number = 0;
|
|
|
|
|
|
@Local tskEditTitle: string = '';
|
|
|
|
|
|
@Local tskEditNote: string = '';
|
|
|
|
|
|
@Local tskEditDueAt: string = '';
|
|
|
|
|
|
@Local tskEditBoardId: number = 0;
|
|
|
|
|
|
@Local tskEditCatId: number = 0;
|
|
|
|
|
|
@Local tskEditRepeat: string = '';
|
2026-11-05 12:41:04 +08:00
|
|
|
|
/** 编辑时的原状态(todo/doing/done) */
|
|
|
|
|
|
@Local tskEditStatus: string = 'todo';
|
2026-10-22 16:40:44 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 面板的"重画脉冲"(v79)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 🔴 为什么需要:编辑表单保存成功后,面板里的标题/截止/分类都变了,
|
|
|
|
|
|
* 但面板的数据是它**自己**拉的 —— 宿主只重拉首页列表,它不会跟着变。
|
|
|
|
|
|
* ⚠️ 别用"关掉再打开"代替:会闪一下、还会丢掉滚动位置。
|
|
|
|
|
|
*/
|
|
|
|
|
|
@Local tskTick: number = 0;
|
2026-10-21 20:01:37 +08:00
|
|
|
|
|
2026-10-24 20:22:35 +08:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// v86:纪念日 / 观影 / 联系人 三张卡的新建入口
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
/* 🔴 用户 2026-10-24:「端上也全部都实现。」
|
|
|
|
|
|
------------------------------------------------------------------
|
|
|
|
|
|
背景:Web 端 v81/v82 已经把「新增电影 / 新建纪念日 / 新增成员」搬到首页
|
|
|
|
|
|
卡片上了(`app/HomeCards.php` 里的 `add` + `add_modal`),
|
|
|
|
|
|
但**端上一个都没跟**。用户装机后只能进模块里建 ⇒ 与 Web 端体验不一致。
|
|
|
|
|
|
|
|
|
|
|
|
🔴 端上与 Web 端的**一处结构性差异**(决定了这里为什么不能照抄 `add_modal`):
|
|
|
|
|
|
Web 端是"卡片右上角一颗按钮 → 点开一个预先渲染好的隐藏弹框",
|
|
|
|
|
|
弹框 HTML 在页面里早就存在,靠 `data-fb-add-open` 找容器。
|
|
|
|
|
|
端上没有"预先存在的 DOM" —— 表单是**组件**,要在 `if (开着的标志)` 里
|
|
|
|
|
|
现场挂上去。⇒ 端上是"标志位 + Stack 里挂组件",不是"找容器"。
|
|
|
|
|
|
⚠️ 所以这里**没有** `add_modal` 那种 id 必须逐字一致的问题,
|
|
|
|
|
|
但换成了另一个纪律:**每个表单一个独立的布尔,别复用**。
|
|
|
|
|
|
复用(比如三个表单共用一个 `formOpen`)会让"打开哪一个"取决于
|
|
|
|
|
|
一堆隐式状态,而且 `backOnce` 没法按层关。
|
|
|
|
|
|
|
|
|
|
|
|
⚠️ 三张卡的数据源**不是** `/home`:
|
|
|
|
|
|
`/home` 只返回日程/任务/纪念日提醒/往年今天,不含观影列表和名册。
|
|
|
|
|
|
⇒ 观影与联系人走**端上本地库**(`FbMovieRepo` / `FbPersonRepo`,
|
|
|
|
|
|
它们由 `syncAll` 定期同步)。这是端上的既定做法(电影页、名册页
|
|
|
|
|
|
也都是读本地库),不是本轮新增的临时方案。 */
|
|
|
|
|
|
|
|
|
|
|
|
/** 「新建 / 编辑纪念日」表单开着(首页只用新建;editingId=0) */
|
|
|
|
|
|
@Local dayCreating: boolean = false;
|
|
|
|
|
|
/** 纪念日表单要的关联人名单(懒拉,与纪念日页同一个来源) */
|
|
|
|
|
|
@Local dayRoster: FbRosterPerson[] = [];
|
|
|
|
|
|
@Local dayLoadingRoster: boolean = false;
|
|
|
|
|
|
|
|
|
|
|
|
/** 「新增电影(记一场)」表单开着 */
|
|
|
|
|
|
@Local mvCreating: boolean = false;
|
|
|
|
|
|
/** 观影卡要显示的最近几条(本地库) */
|
|
|
|
|
|
@Local mvRecent: FbMovieListItem[] = [];
|
|
|
|
|
|
/** 影院列表(`FbMovieNewSheet` 的必填项之一,懒拉) */
|
|
|
|
|
|
@Local mvCinemas: FbCinemaItem[] = [];
|
|
|
|
|
|
@Local mvLoading: boolean = false;
|
|
|
|
|
|
|
|
|
|
|
|
/** 「新增成员」表单开着 */
|
|
|
|
|
|
@Local pfCreating: boolean = false;
|
|
|
|
|
|
/** 联系人卡要显示的人(本地库前 N 个) */
|
|
|
|
|
|
@Local pfRecent: FbRosterPerson[] = [];
|
|
|
|
|
|
/** 名册总人数(卡片底部"共 N 人"用) */
|
|
|
|
|
|
@Local pfTotal: number = 0;
|
|
|
|
|
|
@Local pfLoading: boolean = false;
|
|
|
|
|
|
|
2026-10-21 20:01:37 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 详情卡开着 ⇒ 底部悬浮快捷栏让位。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 与任务页/日程页同一套机制(`sheetCount` 计数):悬浮栏在**更外层
|
|
|
|
|
|
* `Index` 的 Stack** 里,层级比本页高 ⇒ 不让位就会盖住卡片底部的按钮、
|
|
|
|
|
|
* 还把点击抢走(名册/日程页都踩过)。
|
2026-10-24 20:22:35 +08:00
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 新增的表单标志位**必须**加进这个列表:漏一个的后果是
|
|
|
|
|
|
* 表单开着时悬浮栏还浮在上面,把表单底部的「保存」按钮盖住 ——
|
|
|
|
|
|
* 现象是"表单填完了点不到保存"(v79 任务编辑表单踩过同型)。
|
2026-10-21 20:01:37 +08:00
|
|
|
|
*/
|
2026-10-24 20:22:35 +08:00
|
|
|
|
@Monitor('evtBase', 'tskBase', 'tskEditOpen', 'calCreating', 'calEditing',
|
2026-11-05 13:43:03 +08:00
|
|
|
|
'mvDetail', 'pfDetail', 'dayDetail', 'memDetail',
|
2026-10-24 20:22:35 +08:00
|
|
|
|
'dayCreating', 'mvCreating', 'pfCreating')
|
2026-10-21 20:01:37 +08:00
|
|
|
|
onDetailChanged(): void {
|
2026-10-24 19:14:18 +08:00
|
|
|
|
this.app.sheetCount = (this.evtBase !== null || this.tskBase !== null
|
2026-11-05 13:43:03 +08:00
|
|
|
|
|| this.mvDetail !== null || this.pfDetail !== null
|
|
|
|
|
|
|| this.dayDetail !== null || this.memDetail !== null
|
2026-10-24 20:22:35 +08:00
|
|
|
|
|| this.tskEditOpen || this.calCreating || this.calEditing
|
|
|
|
|
|
|| this.dayCreating || this.mvCreating || this.pfCreating) ? 1 : 0;
|
2026-10-21 20:01:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-18 03:35:39 +08:00
|
|
|
|
aboutToAppear(): void {
|
2026-11-05 12:41:04 +08:00
|
|
|
|
/* 诊断设施在这里初始化:地址联想发生在编辑弹窗里,
|
|
|
|
|
|
那时用户可能还没点过导航 ⇒ 不能等导航时才 init。 */
|
|
|
|
|
|
FbDiag.init(this.getUIContext().getHostContext());
|
2026-10-18 03:35:39 +08:00
|
|
|
|
// 显示方式来自日程设置(启动时已预载进内存,可同步读)
|
|
|
|
|
|
this.calStyle = FB_CAL_SETTINGS.getStyle();
|
2026-10-21 20:01:37 +08:00
|
|
|
|
/* 🔴 v73:**首页也要登记返回手势**。
|
|
|
|
|
|
原来首页根本没登记过 —— 因为以前它没有浮层,"返回"就该由 `Index` 处理
|
|
|
|
|
|
(回上一个模块 / 退到桌面)。现在多了两张详情卡,不登记的话:
|
|
|
|
|
|
卡开着按返回 ⇒ **卡片留在屏幕上**、模块却被切走了,回来还是那张卡。
|
|
|
|
|
|
⚠️ 登记/注销必须成对(见 `common/FbBack.ets` 的纪律)。 */
|
|
|
|
|
|
FB_BACK.register('home', (): boolean => this.backOnce());
|
2026-10-18 03:35:39 +08:00
|
|
|
|
this.load();
|
2026-10-24 20:22:35 +08:00
|
|
|
|
/* v86:观影 / 联系人两张卡走端上本地库,与 `/home` 无关 ⇒ 单独拉、不 await。
|
|
|
|
|
|
⚠️ 必须**不 await**:本地库首次打开或库很大时读一次要几百毫秒,
|
|
|
|
|
|
串在上面会让首页首屏白等(而这两张卡是锦上添花)。 */
|
|
|
|
|
|
this.loadSideCards();
|
2026-10-18 03:35:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-21 20:01:37 +08:00
|
|
|
|
aboutToDisappear(): void {
|
|
|
|
|
|
FB_BACK.unregister('home');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 返回手势:先关详情卡,再交给 `Index`(返回 true = 已消费) */
|
|
|
|
|
|
private backOnce(): boolean {
|
2026-10-22 16:40:44 +08:00
|
|
|
|
/* v79:**后打开的先关** —— 编辑表单是压在面板之上的,返回一次只该关它。
|
|
|
|
|
|
顺序反了会"按一次返回,两层一起没了"(用户会以为点错了)。 */
|
|
|
|
|
|
if (this.tskEditOpen) {
|
|
|
|
|
|
this.tskEditOpen = false;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
2026-10-24 20:22:35 +08:00
|
|
|
|
/* v84 新建日程 / v86 编辑日程:都是**独立表单**(不叠在详情卡之上,
|
|
|
|
|
|
但详情卡在它们下面还留着),所以必须排在 `evtBase` 之前判 ——
|
|
|
|
|
|
否则按一次返回会"跳过表单直接关掉详情卡",表单还杵在屏幕上。 */
|
|
|
|
|
|
if (this.calEditing) {
|
|
|
|
|
|
this.calEditing = false;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
2026-10-24 19:14:18 +08:00
|
|
|
|
if (this.calCreating) {
|
|
|
|
|
|
this.calCreating = false;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
2026-10-24 20:22:35 +08:00
|
|
|
|
/* v86:三张卡的新建表单。与上面同理 —— 必须排在详情卡/面板之前,
|
|
|
|
|
|
否则按一次返回会"跳过表单",表单还留在屏幕上。 */
|
|
|
|
|
|
if (this.dayCreating) {
|
|
|
|
|
|
this.dayCreating = false;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (this.mvCreating) {
|
|
|
|
|
|
this.mvCreating = false;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (this.pfCreating) {
|
|
|
|
|
|
this.pfCreating = false;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
2026-10-21 20:01:37 +08:00
|
|
|
|
if (this.evtBase !== null) {
|
|
|
|
|
|
this.closeEvent();
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (this.tskBase !== null) {
|
|
|
|
|
|
this.closeTask();
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-18 03:35:39 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 拉首页数据。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 首页**只要一个接口**了(v54)—— 原来还要并发拉 `/calendar`(未来七天)
|
|
|
|
|
|
* 和 `/tasks`(任务),因为那两块数据接口没给。
|
|
|
|
|
|
* 现在服务端 `/home` 把它们都备齐了,少两个请求就是首页更快一截。
|
|
|
|
|
|
*/
|
|
|
|
|
|
private async load(): Promise<void> {
|
|
|
|
|
|
await loadModule<FbHomeData>('home', parseHome,
|
|
|
|
|
|
(d: FbHomeData): void => {
|
|
|
|
|
|
this.todayEvents = d.todayEvents;
|
|
|
|
|
|
this.tasks = d.tasks;
|
|
|
|
|
|
this.greeting = d.greeting;
|
|
|
|
|
|
this.todayLabel = d.todayLabel;
|
|
|
|
|
|
this.weekday = d.weekday;
|
|
|
|
|
|
this.lunarShort = d.lunarShort;
|
|
|
|
|
|
this.marks = d.marks;
|
|
|
|
|
|
this.weather = d.weather;
|
|
|
|
|
|
this.statToday = d.stat.todayEvents;
|
|
|
|
|
|
this.statDoing = d.stat.doing;
|
|
|
|
|
|
this.statTodo = d.stat.todo;
|
|
|
|
|
|
this.statOverdue = d.stat.overdue;
|
|
|
|
|
|
this.memories = d.memories;
|
2026-10-18 20:14:40 +08:00
|
|
|
|
this.recentDays = d.recentDays;
|
2026-10-18 20:58:40 +08:00
|
|
|
|
/* 🔴🔴 v59b(用户 2026-10-18):「提醒按钮,明明没提醒,怎么还显示个 2?」
|
|
|
|
|
|
*
|
|
|
|
|
|
* 真根因:`app.unread` **从来没接过数据** —— 它只是 `FbAppState` 里的
|
|
|
|
|
|
* 一个写死的初值 `@Trace unread: number = 2;`,而全工程**只有一处**
|
|
|
|
|
|
* 给它赋值(通知页的「全部已读」把它改成 0)。
|
|
|
|
|
|
* ⇒ 于是"从没进过通知页"的用户**永远看到 2**;进了通知页又变成
|
|
|
|
|
|
* "永远是 0"(把真实未读也盖掉了)。两个方向都错。
|
|
|
|
|
|
*
|
|
|
|
|
|
* `/home` 本来就返回权威的 `unread`(服务端 `Notification::unreadCount`,
|
|
|
|
|
|
* 与通知页同一个来源)—— 这里把它同步进全局状态,铃铛角标就真了。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 首页是启动后必到的页面(默认模块),所以这一个赋值点就够
|
|
|
|
|
|
* "进应用看到真数字";进通知页后由那边继续维护(标记已读会变)。
|
|
|
|
|
|
*/
|
|
|
|
|
|
this.app.unread = d.unread;
|
2026-10-18 03:35:39 +08:00
|
|
|
|
},
|
|
|
|
|
|
(note: string): void => {
|
2026-10-24 20:22:35 +08:00
|
|
|
|
/* ⚠️ 同 `loadTaskMeta` 的那条:这是首屏自动加载的后台失败,
|
|
|
|
|
|
不是用户点击的直接后果 ⇒ **故意不走 `note()`**,不弹 Toast。 */
|
2026-10-18 03:35:39 +08:00
|
|
|
|
this.syncNote = note;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-10-11 22:07:48 +08:00
|
|
|
|
|
2026-10-21 20:01:37 +08:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// v73:日程详情卡
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 点一条日程 → 弹详情卡(**不跳模块**)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 逻辑与日程页的 `openEvent` 保持同一套(那是最成熟的一份,别另写一套):
|
|
|
|
|
|
* ① 先用列表那条垫一份"壳"(标题/时间/地点/备注够画,用户立刻看到内容);
|
|
|
|
|
|
* ② 可编辑条目再拉 `/events/<id>` 补 `rrule` / `reminder` / 精确 `ends_at`;
|
|
|
|
|
|
* ③ 拉不到就保留壳 + 一行小字说明(**不要**把整张卡变成"读不到")。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 衍生条目(农历/节气/节假日)**没有 `/events/<id>` 可拉**
|
|
|
|
|
|
* (服务端 `ref_id` 是 0,它们不是 events 表里的行)⇒ 不要发那个请求。
|
|
|
|
|
|
*/
|
|
|
|
|
|
private openEvent(e: FbCalEvent): void {
|
|
|
|
|
|
this.evtBase = e;
|
|
|
|
|
|
this.evtFull = null;
|
|
|
|
|
|
this.evtError = '';
|
2026-10-29 20:39:52 +08:00
|
|
|
|
/* 🔴 2026-10-29(工单 #31):详情卡的「编辑日程」按 `calBooks[calId].canWrite` 判定,
|
|
|
|
|
|
而 `calBooks` 原本是懒加载(只在新建/编辑时才拉)⇒ 刚进详情时它还是空数组,
|
|
|
|
|
|
会把**本来能编辑**的日程也误判成只读。
|
|
|
|
|
|
这里顺手拉一次(拉过就复用,`ensureCalBooks` 内部有缓存判断),
|
|
|
|
|
|
不 await —— 详情内容先出来,按钮随 books 到位再显示,不拖慢打开速度。 */
|
|
|
|
|
|
if (this.calBooks.length === 0) {
|
|
|
|
|
|
this.ensureCalBooks().catch((): void => {
|
|
|
|
|
|
/* 拉不到就维持"不给编辑入口",比让用户白改一遍好 */
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-10-21 20:01:37 +08:00
|
|
|
|
if (!isEditableEvent(e)) {
|
|
|
|
|
|
this.evtLoading = false;
|
|
|
|
|
|
this.evtFull = this.shellOf(e);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const token: string = FB_TOKEN_STORE.get();
|
|
|
|
|
|
if (token === '') {
|
|
|
|
|
|
this.evtLoading = false;
|
|
|
|
|
|
this.evtFull = this.shellOf(e);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.evtLoading = true;
|
|
|
|
|
|
this.evtToken += 1;
|
|
|
|
|
|
const my: number = this.evtToken;
|
|
|
|
|
|
FbCalRepo.inst().fetchEvent(e.id, token).then((full: FbCalEventFull | null): void => {
|
|
|
|
|
|
if (my !== this.evtToken) {
|
|
|
|
|
|
return; // 已经有更新的一次点击了,这份丢弃
|
|
|
|
|
|
}
|
|
|
|
|
|
this.evtLoading = false;
|
|
|
|
|
|
if (full === null) {
|
|
|
|
|
|
this.evtError = '读不到详情';
|
|
|
|
|
|
this.evtFull = this.shellOf(e);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.evtFull = full;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 列表那条 → 完整字段的壳(只垫列表**已知**的字段,别编) */
|
2026-11-05 16:21:16 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 把列表条目垫成一份"详情壳"。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 🔴 #69(用户 2026-11-05):「点击行程里旅行日历本里的信息时,弹出来的那个框…
|
|
|
|
|
|
* 还是没有出发地和目的地。」
|
|
|
|
|
|
*
|
|
|
|
|
|
* 为什么这个壳很重要:`openEvent()` 里
|
|
|
|
|
|
* `if (!isEditableEvent(e)) { this.evtFull = this.shellOf(e); return; }`
|
|
|
|
|
|
* —— 行程条目的 `kind` 是 `'trip'` 不是 `'custom'`,
|
|
|
|
|
|
* **永远走这条捷径、压根不请求详情接口**(而且也没有意义:
|
|
|
|
|
|
* 行程节点的数据在 `trip_nodes` 表里,`Calendar::event($id)` 查的是
|
|
|
|
|
|
* `calendar_events`,对节点 id 必然查不到)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⇒ 位置信息**只能**靠这个壳带过去。原来只塞了 `location: b.place`,
|
|
|
|
|
|
* 而行程节点的 `place` 是空串(它的位置概念是"从哪到哪"),
|
|
|
|
|
|
* 于是详情卡里一行位置信息都没有。
|
|
|
|
|
|
*/
|
2026-10-21 20:01:37 +08:00
|
|
|
|
private shellOf(b: FbCalEvent): FbCalEventFull {
|
|
|
|
|
|
const shell: FbCalEventFull = {
|
|
|
|
|
|
id: b.id, calendar_id: b.calId, title: b.title, location: b.place,
|
|
|
|
|
|
starts_at: b.date + ' ' + (b.allDay ? '00:00:00' : (b.time + ':00')),
|
|
|
|
|
|
ends_at: b.endTime !== '' ? (b.date + ' ' + b.endTime + ':00') : '',
|
|
|
|
|
|
all_day: b.allDay, note: b.note, rrule: '', reminder_minutes: '',
|
2026-11-05 16:21:16 +08:00
|
|
|
|
status: 'confirmed', transp: false, created_by: 0,
|
|
|
|
|
|
/* #69:行程条目的「从哪到哪」—— 服务端不给 location,
|
|
|
|
|
|
不给这几个字段的话详情卡里就没有任何位置信息 */
|
|
|
|
|
|
fromPlace: b.fromPlace, toPlace: b.toPlace,
|
|
|
|
|
|
fromLng: b.fromLng, fromLat: b.fromLat, toLng: b.toLng, toLat: b.toLat
|
2026-10-21 20:01:37 +08:00
|
|
|
|
};
|
|
|
|
|
|
return shell;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private closeEvent(): void {
|
|
|
|
|
|
this.evtBase = null;
|
|
|
|
|
|
this.evtFull = null;
|
|
|
|
|
|
this.evtError = '';
|
|
|
|
|
|
this.evtLoading = false;
|
|
|
|
|
|
this.evtToken += 1; // 丢弃在途请求
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 详情卡上点「导航」/ 点地点行 → 拉起地图 */
|
2026-11-05 16:21:16 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 去导航(#69:改成接受 `navTarget`)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 原来是无参的 —— 只读 `evtFull.location`。行程条目没有 location,
|
|
|
|
|
|
* 且详情卡里现在有「出发地」「目的地」两行,得知道点的是哪一行。
|
|
|
|
|
|
* ⇒ 详情卡把那一行的地名(带坐标时是 `lat,lng?地名`)直接交进来。
|
|
|
|
|
|
*/
|
|
|
|
|
|
private navigateEvent(navTarget: string): void {
|
2026-10-21 20:01:37 +08:00
|
|
|
|
const placeOfFull: string = this.evtFull !== null ? this.evtFull.location : '';
|
|
|
|
|
|
const placeOfBase: string = this.evtBase !== null ? this.evtBase.place : '';
|
2026-11-05 16:21:16 +08:00
|
|
|
|
/* 优先用详情卡传来的目标;它为空(老路径 / 极少数情况)才退回原来那套推导 */
|
|
|
|
|
|
const place: string = navTarget !== ''
|
|
|
|
|
|
? navTarget
|
|
|
|
|
|
: (placeOfFull !== '' ? placeOfFull : placeOfBase);
|
2026-10-21 20:01:37 +08:00
|
|
|
|
const title: string = this.evtFull !== null ? this.evtFull.title
|
|
|
|
|
|
: (this.evtBase !== null ? this.evtBase.title : '');
|
|
|
|
|
|
/* ⚠️ 必须是 `UIAbilityContext`(`startAbility` 只在这个上下文上有;
|
|
|
|
|
|
用 `common.Context` 编译就报 "Property 'startAbility' does not exist",
|
|
|
|
|
|
名册拨号那次实测过)。 */
|
|
|
|
|
|
const ctx: common.UIAbilityContext | undefined =
|
|
|
|
|
|
this.getUIContext().getHostContext() as common.UIAbilityContext | undefined;
|
2026-11-03 10:28:28 +08:00
|
|
|
|
/* ⚠️ `fbOpenNavigation` 现在是 async(内部要 await 地理编码),
|
|
|
|
|
|
不接住 rejection 会报未处理异常。这里明确咽掉并提示 ——
|
|
|
|
|
|
导航失败不该把页面弄崩。 */
|
2026-11-05 12:41:04 +08:00
|
|
|
|
FbDiag.init(ctx);
|
2026-11-03 10:28:28 +08:00
|
|
|
|
fbOpenNavigation(ctx, place, title).catch((err: Error): void => {
|
|
|
|
|
|
hilog.warn(0x0000, 'FbNav', '导航调用异常:%{public}s', JSON.stringify(err));
|
|
|
|
|
|
});
|
2026-10-21 20:01:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-10-24 20:22:35 +08:00
|
|
|
|
* 日程详情卡上点「编辑」→ **就地**打开编辑表单(v86)。
|
2026-10-21 20:01:37 +08:00
|
|
|
|
*
|
2026-10-24 20:22:35 +08:00
|
|
|
|
* 🔴 历史(别走回头路):
|
|
|
|
|
|
* v73 把"点日程跳模块"改成"就地弹详情卡",用户原话是
|
|
|
|
|
|
* 「不要进入到日程里面」。
|
|
|
|
|
|
* v84 我却把详情卡上的「编辑」做成了 `closeEvent() + switchModule('calendar')`
|
|
|
|
|
|
* —— 于是点「编辑」还是会跳走,只是跳得晚一点。用户这轮说「端上也全部都实现」,
|
|
|
|
|
|
* 指的就是这类"看起来做了、其实还是老行为"的地方。
|
|
|
|
|
|
* ⇒ 现在与任务的 `openTaskEdit` 对齐:卡片不关,直接在上面叠一张编辑表单。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 详情卡**不关**(`evtBase` 保留):关掉再开表单会让用户觉得"卡片没了",
|
|
|
|
|
|
* 而且保存后还要靠它在原位重拉一次详情。表单叠在它上面,
|
|
|
|
|
|
* 关闭表单就回到详情卡 —— 与任务面板 → 编辑表单的层级关系一致。
|
2026-10-21 20:01:37 +08:00
|
|
|
|
*/
|
|
|
|
|
|
private editEventFromDetail(): void {
|
2026-10-24 20:22:35 +08:00
|
|
|
|
this.openEventEdit();
|
2026-10-21 20:01:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
2026-10-22 16:40:44 +08:00
|
|
|
|
// v79:任务面板(v73 的"详情卡"升级成可就地处理的面板)
|
2026-10-21 20:01:37 +08:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
private openTask(t: FbTaskItem): void {
|
|
|
|
|
|
this.tskBase = t;
|
2026-10-22 16:40:44 +08:00
|
|
|
|
/* 看板/分类表按需拉一次(拉过就不再拉;它是"元数据",几条而己) */
|
|
|
|
|
|
this.loadTaskMeta();
|
2026-10-21 20:01:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private closeTask(): void {
|
|
|
|
|
|
this.tskBase = null;
|
2026-10-22 16:40:44 +08:00
|
|
|
|
this.tskEditOpen = false;
|
2026-10-21 20:01:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-05 13:43:03 +08:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// #62/#63/#64/#65:各区块的详情卡开/关
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 打开电影详情(#63 / #65 共用)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 🔴 用户:「"最近观影"点了之后没反应…点了之后应该有反应,
|
|
|
|
|
|
* 要么弹出来详细信息。」
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 两段式:先立即用「列表里已有的评分/短评数」把卡片显示出来
|
|
|
|
|
|
* (`loading=true`),再异步拉完整详情。
|
|
|
|
|
|
* 不许等详情回来才开卡 —— 网络慢时用户会觉得"点了没反应",
|
|
|
|
|
|
* 那正是这次要修的问题。
|
|
|
|
|
|
*/
|
|
|
|
|
|
private async openMovieDetail(id: number, seed: FbMovieListItem | null): Promise<void> {
|
|
|
|
|
|
const token: string = FB_TOKEN_STORE.get();
|
|
|
|
|
|
if (token === '') {
|
|
|
|
|
|
this.mvDetailError = '未连接服务器,详情需要联网查看';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.mvSeed = seed;
|
|
|
|
|
|
this.mvDetail = null;
|
|
|
|
|
|
this.mvDetailError = '';
|
|
|
|
|
|
/* 先开卡片(带加载态),再拉数据 —— 网络慢时用户不能"点了没反应" */
|
|
|
|
|
|
this.mvDetailLoading = true;
|
|
|
|
|
|
|
|
|
|
|
|
/* ⚠️ 照 `FbMoviesPage.openDetail` 的成熟写法:
|
|
|
|
|
|
用 `FbMovieRepo.inst().detail(id, token, seed)`,它内部处理了
|
|
|
|
|
|
缓存/降级/字段兜底;不要自己拼 `/api/v1/movies/<id>` 再解析
|
|
|
|
|
|
(`parseMovieDetail` 不导出,且 repo 里已经算好了一堆派生字段)。 */
|
|
|
|
|
|
/* ⚠️ seed 可能是 null,而 detail() 要 FbMovieListItem | undefined
|
|
|
|
|
|
⇒ null 转 undefined */
|
|
|
|
|
|
const d: FbMovieDetail = await FbMovieRepo.inst().detail(id, token,
|
|
|
|
|
|
seed !== null ? seed : undefined);
|
|
|
|
|
|
/* 用户可能在等待期间关掉了卡片 ⇒ 回来时先确认还开着 */
|
|
|
|
|
|
if (this.mvDetail === null && !this.mvDetailLoading && this.mvSeed === null) {
|
|
|
|
|
|
return; // 已关闭
|
|
|
|
|
|
}
|
|
|
|
|
|
this.mvDetailLoading = false;
|
|
|
|
|
|
if (!d.ok) {
|
|
|
|
|
|
this.mvDetailError = d.error;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.mvDetail = d;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private closeMovieDetail(): void {
|
|
|
|
|
|
this.mvDetail = null;
|
|
|
|
|
|
this.mvDetailLoading = false;
|
|
|
|
|
|
this.mvDetailError = '';
|
|
|
|
|
|
this.mvSeed = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 打开人员详情(#64)。与名册点人的效果一致。 */
|
|
|
|
|
|
private openPersonDetail(p: FbRosterPerson): void {
|
|
|
|
|
|
this.pfDetail = p;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private closePersonDetail(): void {
|
|
|
|
|
|
this.pfDetail = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 打开纪念日详情(#63)。 */
|
|
|
|
|
|
private openDayDetail(d: FbRecentDay): void {
|
|
|
|
|
|
this.dayDetail = d;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private closeDayDetail(): void {
|
|
|
|
|
|
this.dayDetail = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 打开「往年的今天」条目(#65)—— **按来源分发**。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 🔴 用户:「点完其中一个之后,要么弹出来详细信息,
|
|
|
|
|
|
* 比方说它来自电影,就应该弹出来跟电影有关的弹框卡片。」
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ `FbMemoryItem.kind` 只有 `movie` / `trip_node`:
|
|
|
|
|
|
* · `movie` → 走 `openMovieDetail`(id 就是 events.id)
|
|
|
|
|
|
* · `trip_node` → 行程卡需要 tripId + nodeId,而 memory 只有 nodeId
|
|
|
|
|
|
* ⇒ **拿不到 tripId 就没法开行程卡**,
|
|
|
|
|
|
* 退回通用摘要卡(`memDetail` 自己渲染)。
|
|
|
|
|
|
* 弹错类型的卡比不弹更糟,所以这里宁可保守。
|
|
|
|
|
|
*/
|
|
|
|
|
|
private openMemoryDetail(m: FbMemoryItem): void {
|
|
|
|
|
|
if (m.kind === 'movie') {
|
|
|
|
|
|
/* 电影:直接开电影详情卡(id 是 events.id,接口认这个) */
|
|
|
|
|
|
this.openMovieDetail(m.id, null);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
/* 其它(trip_node 等):通用摘要卡 */
|
|
|
|
|
|
this.memDetail = m;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private closeMemoryDetail(): void {
|
|
|
|
|
|
this.memDetail = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 滚到指定 y(#59)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 减去 `topBar` 的高度(约 88)会在某些机型上滚过头 ——
|
|
|
|
|
|
* 这里直接用区块自己的 y,顶到可视区顶部即可。
|
|
|
|
|
|
* 若实机发现区块标题被顶栏遮住,再减一个固定偏置。
|
|
|
|
|
|
*/
|
|
|
|
|
|
private scrollToY(y: number): void {
|
|
|
|
|
|
if (y <= 0) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.homeScroller.scrollTo({ xOffset: 0, yOffset: y, animation: { duration: 280 } });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 概述格「今天日程」→ 今天的日程区块 */
|
|
|
|
|
|
private jumpToEvents(): void {
|
|
|
|
|
|
this.scrollToY(this.yEvents);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 概述格「在做 / 任务 / 逾期」→ 近期任务区块(三者都是任务状态计数) */
|
|
|
|
|
|
private jumpToTasks(): void {
|
|
|
|
|
|
this.scrollToY(this.yTasks);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 「往年的今天」的**通用摘要卡**(#65)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 🔴 用户:「点完其中一个之后,要么弹出来详细信息,
|
|
|
|
|
|
* 比方说它来自电影,就应该弹出来跟电影有关的弹框卡片。
|
|
|
|
|
|
* 然后我滑下去之后可以看信息,然后可以关闭它。」
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 电影类走 `openMovieDetail`(弹真正的电影卡),**不到这里**。
|
|
|
|
|
|
* 这里只处理 `trip_node` 这类拿不到 tripId 的来源 ——
|
|
|
|
|
|
* 行程卡需要 tripId + nodeId,而 memory 只有 nodeId,
|
|
|
|
|
|
* 硬开只会弹个空卡。宁可给一份诚实的摘要。
|
|
|
|
|
|
* ⚠️ 内容可滚动(用户说"我滑下去之后可以看信息")。
|
|
|
|
|
|
*/
|
|
|
|
|
|
@Builder
|
|
|
|
|
|
memorySheet(m: FbMemoryItem) {
|
|
|
|
|
|
Column()
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.height('100%')
|
|
|
|
|
|
.backgroundColor('#66000000')
|
|
|
|
|
|
.onClick((): void => {
|
|
|
|
|
|
this.closeMemoryDetail();
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
Column() {
|
|
|
|
|
|
FbSheetRing({ radius: FbMetric.radius, ringColor: this.calAccent() }) {
|
|
|
|
|
|
Column({ space: 12 }) {
|
|
|
|
|
|
/* 标题栏 */
|
|
|
|
|
|
Row() {
|
|
|
|
|
|
Text('往年今天')
|
|
|
|
|
|
.fontSize(FbMetric.fontBody)
|
|
|
|
|
|
.fontWeight(FontWeight.Bold)
|
|
|
|
|
|
.fontColor(this.calAccent())
|
|
|
|
|
|
Blank()
|
|
|
|
|
|
Text('✕')
|
|
|
|
|
|
.fontSize(FbMetric.fontBody)
|
|
|
|
|
|
.fontColor(FbColors.muted)
|
|
|
|
|
|
.padding(8)
|
|
|
|
|
|
.onClick((): void => {
|
|
|
|
|
|
this.closeMemoryDetail();
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
|
|
|
|
|
|
Divider().color(FbColors.line2)
|
|
|
|
|
|
|
|
|
|
|
|
/* 内容(可滚动) */
|
|
|
|
|
|
Scroll() {
|
|
|
|
|
|
Column({ space: 10 }) {
|
|
|
|
|
|
Text(m.title)
|
|
|
|
|
|
.fontSize(FbMetric.fontBody)
|
|
|
|
|
|
.fontWeight(FontWeight.Medium)
|
|
|
|
|
|
.fontColor(FbColors.ink)
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
|
|
|
|
|
|
Text(m.startsAt)
|
|
|
|
|
|
.fontSize(FbMetric.fontSmall)
|
|
|
|
|
|
.fontColor(FbColors.muted)
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
|
|
|
|
|
|
Text(m.yearsAgo.toString() + ' 年前的今天')
|
|
|
|
|
|
.fontSize(FbMetric.fontSmall)
|
|
|
|
|
|
.fontColor(this.calAccent())
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
|
|
|
|
|
|
/* 说明:为什么这里不是完整的行程卡 */
|
|
|
|
|
|
if (m.kind === 'trip_node') {
|
|
|
|
|
|
Text('这条来自旅程。要看完整的行程详情,请到「旅程」里打开对应旅程。')
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(FbColors.faint)
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding({ top: 4, bottom: 8 })
|
|
|
|
|
|
}
|
|
|
|
|
|
.constraintSize({ maxHeight: 420 })
|
|
|
|
|
|
.scrollBar(BarState.Off)
|
|
|
|
|
|
.align(Alignment.TopStart)
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding({ left: 18, right: 18, top: 18, bottom: 32 })
|
|
|
|
|
|
.alignItems(HorizontalAlign.Start)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('90%')
|
|
|
|
|
|
.constraintSize({ maxHeight: '80%' })
|
|
|
|
|
|
.margin({ top: 8, bottom: 8 })
|
|
|
|
|
|
.onClick((): void => {
|
|
|
|
|
|
/* 吃掉点击,避免穿透到底下的遮罩把自己关了 */
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 16:40:44 +08:00
|
|
|
|
/** 拉全部看板 + 分类(面板的"挪分组"、表单的"分类"都要用) */
|
|
|
|
|
|
private async loadTaskMeta(): Promise<void> {
|
|
|
|
|
|
if (this.tskBoards.length > 0) {
|
2026-10-21 20:01:37 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-10-22 16:40:44 +08:00
|
|
|
|
await loadPath<FbTaskMeta>('tasks_meta', '/api/v1/tasks?view=boards', parseTaskMeta,
|
|
|
|
|
|
(m: FbTaskMeta): void => {
|
|
|
|
|
|
this.tskBoards = m.boards;
|
|
|
|
|
|
this.tskCats = m.cats;
|
|
|
|
|
|
},
|
|
|
|
|
|
(note: string): void => {
|
|
|
|
|
|
/* ⚠️ 失败**不拦路**:面板里状态/子任务/留言都还能用,
|
|
|
|
|
|
只是"换看板"那一排会少几个(甚至只剩默认看板)。 */
|
2026-10-24 20:22:35 +08:00
|
|
|
|
/* ⚠️ 这里**故意不走 `note()`**:它是首屏自动加载的后台失败,
|
|
|
|
|
|
不是用户点击的直接后果。启动就弹一个 Toast 会让人莫名其妙,
|
|
|
|
|
|
而且这条失败不影响主流程(面板本身能用)。 */
|
2026-10-22 16:40:44 +08:00
|
|
|
|
this.syncNote = note;
|
|
|
|
|
|
});
|
2026-10-21 20:01:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 16:40:44 +08:00
|
|
|
|
/** 面板里点「编辑」→ 在首页就地打开编辑表单(v79,不再跳任务页) */
|
|
|
|
|
|
private openTaskEdit(d: FbTaskDetail): void {
|
|
|
|
|
|
this.tskEditId = d.base.id;
|
|
|
|
|
|
this.tskEditTitle = d.base.title;
|
|
|
|
|
|
this.tskEditNote = d.base.note;
|
|
|
|
|
|
this.tskEditDueAt = d.base.dueAt;
|
|
|
|
|
|
this.tskEditBoardId = d.base.boardId;
|
|
|
|
|
|
this.tskEditCatId = d.categoryId;
|
|
|
|
|
|
this.tskEditRepeat = d.repeat;
|
2026-11-05 12:41:04 +08:00
|
|
|
|
this.tskEditStatus = d.base.status !== '' ? d.base.status : 'todo';
|
2026-10-22 16:40:44 +08:00
|
|
|
|
this.tskEditOpen = true;
|
2026-10-21 20:01:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-24 19:14:18 +08:00
|
|
|
|
/**
|
2026-10-24 20:22:35 +08:00
|
|
|
|
* v86:「近期任务」区块点「+ 新建」→ 就地打开新建任务表单。
|
2026-10-24 19:14:18 +08:00
|
|
|
|
*
|
2026-10-24 20:22:35 +08:00
|
|
|
|
* 🔴 用户第一轮原话里就有这条(「网页端,首页……在全部任务旁边,放一个新建任务的功能」),
|
|
|
|
|
|
* Web 端 v78 做了、端上一直缺 ⇒ 本轮补齐。
|
2026-10-24 19:14:18 +08:00
|
|
|
|
*
|
2026-10-24 20:22:35 +08:00
|
|
|
|
* ⚠️ 与 `openTaskEdit` 的区别**只在回填字段**:新建必须把 7 个字段**全部清零**。
|
|
|
|
|
|
* 不清零的话,上一次编辑过的标题/看板/分类会漏进新建表单里 ——
|
|
|
|
|
|
* 表现为"新建任务时标题已经填好了上次那条的名字",
|
|
|
|
|
|
* 用户很难理解,而且会真的把它建出来(脏数据)。
|
|
|
|
|
|
* ⇒ 清零逻辑集中在这一个方法里,别在别处再开一条通路。
|
|
|
|
|
|
* ⚠️ 先拉看板/分类(`loadTaskMeta`):新建表单里"属于哪个看板 / 什么分类"是选择项,
|
|
|
|
|
|
* 没有元数据那两排就是空的。拉取失败**不拦路**(照常开表单,用户可以先写标题)。
|
2026-10-24 19:14:18 +08:00
|
|
|
|
*/
|
2026-10-24 20:22:35 +08:00
|
|
|
|
private async openTaskCreate(): Promise<void> {
|
|
|
|
|
|
if (this.tskEditOpen) {
|
2026-10-24 19:14:18 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-10-24 20:22:35 +08:00
|
|
|
|
this.tskEditId = 0;
|
|
|
|
|
|
this.tskEditTitle = '';
|
|
|
|
|
|
this.tskEditNote = '';
|
|
|
|
|
|
this.tskEditDueAt = '';
|
|
|
|
|
|
this.tskEditBoardId = 0;
|
|
|
|
|
|
this.tskEditCatId = 0;
|
|
|
|
|
|
this.tskEditRepeat = '';
|
2026-11-05 12:41:04 +08:00
|
|
|
|
this.tskEditStatus = 'todo';
|
2026-10-24 20:22:35 +08:00
|
|
|
|
await this.loadTaskMeta();
|
|
|
|
|
|
this.tskEditOpen = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
// v86:纪念日 / 观影 / 联系人 —— 卡片上的「新建」
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 「最近纪念日」区块点「+ 新建」→ 就地打开纪念日表单。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 必须先把 `roster` 拉齐:纪念日表单里"关联谁"是多选,
|
|
|
|
|
|
* 名单空着那一栏就没人可选(但**不拦路** —— 不关联人也能建纪念日)。
|
|
|
|
|
|
* ⚠️ 拉的是**端上本地库**(`FbPersonRepo.list()`),与纪念日页同一个来源;
|
|
|
|
|
|
* 本地库由 `syncAll` 定期同步,所以第一次装机时可能为空 ——
|
|
|
|
|
|
* 那时仍然要能建(纪念日本身不依赖人)。
|
|
|
|
|
|
*/
|
|
|
|
|
|
private async openDayCreate(): Promise<void> {
|
|
|
|
|
|
if (this.dayCreating) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (this.dayRoster.length === 0 && !this.dayLoadingRoster) {
|
|
|
|
|
|
this.dayLoadingRoster = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
this.dayRoster = await FbPersonRepo.inst().list();
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
hilog.warn(DOMAIN, TAG, '首页拉名册失败(纪念日关联人):%{public}s', JSON.stringify(err));
|
|
|
|
|
|
this.dayRoster = [];
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
this.dayLoadingRoster = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
this.dayCreating = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 「观影」区块点「+ 新增」→ 就地打开「记一场」表单。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ `FbMovieNewSheet` 需要 `cinemas`(影院是**必选**的,没有影院建不了)。
|
|
|
|
|
|
* 影院从本地库读 —— 本地库里一条都没有时(刚装机、还没同步过),
|
|
|
|
|
|
* 表单里那一排是空的、存不上。这时**明说一句**别让用户白填:
|
|
|
|
|
|
* 「先去观影模块把影院同步一次」。
|
|
|
|
|
|
* ⚠️ 与日程的 `ensureCalBooks()` 同一个道理:**没有必选项就别开空表单**,
|
|
|
|
|
|
* 否则现象是"填完了点保存,什么都没发生"。
|
|
|
|
|
|
*/
|
|
|
|
|
|
private async openMovieCreate(): Promise<void> {
|
|
|
|
|
|
if (this.mvCreating || this.mvLoading) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.mvLoading = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (this.mvCinemas.length === 0) {
|
|
|
|
|
|
this.mvCinemas = await FbMovieRepo.inst().cinemaList();
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
hilog.warn(DOMAIN, TAG, '首页拉影院失败:%{public}s', JSON.stringify(err));
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
this.mvLoading = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (this.mvCinemas.length === 0) {
|
|
|
|
|
|
this.note('还没有影院可选,先去观影模块里添加一家影院');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.mvCreating = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 「联系人」区块点「+ 新增」→ 就地打开「新增成员」表单。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ `knownTags` 传的是**名册里已有的全部标签**(新建时可以点选复用),
|
|
|
|
|
|
* 不传的话用户只能手打,很容易打出"同事"和"同 事"两个标签。
|
|
|
|
|
|
* ⚠️ 名册可能是空的(第一次用)—— 这不拦路,新建第一个人正是这时候要用的。
|
|
|
|
|
|
*/
|
|
|
|
|
|
private openPersonCreate(): void {
|
|
|
|
|
|
if (this.pfCreating) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.pfCreating = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 拉观影卡 / 联系人卡要展示的数据(都走端上本地库)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 为什么**不 await 在 load() 里串起来**:这两张卡是"锦上添花",
|
|
|
|
|
|
* 拉不到就画空态,绝不该拖慢首页首屏(`/home` 那条才是主路径)。
|
|
|
|
|
|
* ⇒ 独立方法、`catch` 吃掉异常、只写日志。
|
|
|
|
|
|
* ⚠️ 只取**很少几条**:卡片上放 4~5 条就够,多了首页会变成长列表,
|
|
|
|
|
|
* 而"看全部"本来就是进模块的事(Web 端也是这个口径:`moviesData()` 取 8 条)。
|
|
|
|
|
|
*/
|
|
|
|
|
|
private async loadSideCards(): Promise<void> {
|
|
|
|
|
|
this.mvLoading = true;
|
|
|
|
|
|
this.pfLoading = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const mv: FbMovieListItem[] = await FbMovieRepo.inst().list(5);
|
|
|
|
|
|
this.mvRecent = mv;
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
hilog.warn(DOMAIN, TAG, '首页读观影列表失败:%{public}s', JSON.stringify(err));
|
|
|
|
|
|
this.mvRecent = [];
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
this.mvLoading = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
|
|
|
const people: FbRosterPerson[] = await FbPersonRepo.inst().list();
|
|
|
|
|
|
this.pfTotal = people.length;
|
|
|
|
|
|
/* 卡片上只露前 6 个:与 `HomeCards` 的 `contacts` 默认 `n => 8` 同口径,
|
|
|
|
|
|
手机上 6 个(3 列 × 2 行)刚好一屏内不压其他卡片。 */
|
|
|
|
|
|
this.pfRecent = people.slice(0, 6);
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
hilog.warn(DOMAIN, TAG, '首页读名册失败:%{public}s', JSON.stringify(err));
|
|
|
|
|
|
this.pfRecent = [];
|
|
|
|
|
|
this.pfTotal = 0;
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
this.pfLoading = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 给用户说一句话(首页的"为什么点了没反应"出口)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 🔴🔴 2026-10-24 真机事故(本轮造成的,也是本轮修掉的):
|
|
|
|
|
|
* `syncNote` 这个 `@Local` 在本页**只有赋值、没有任何渲染点**——
|
|
|
|
|
|
* `grep this.syncNote` 全文件 5 处,全是 `=`,没有一处 `Text(this.syncNote)`。
|
|
|
|
|
|
* 后果:`ensureCalBooks()` 判"这个账号还没有可写的日程本"时只是**静默 return**,
|
|
|
|
|
|
* 用户点「+ 新建」看到的现象就是**什么都没发生**(按钮确实在、点了也确实进了函数)。
|
|
|
|
|
|
* 这是本项目最难查的一类失败:所有"按钮存在"类断言全绿,功能却用不了。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 对照:账本页 `FbLedgerPage` 里有 `if (this.syncNote !== '') { Text(this.syncNote) }`,
|
|
|
|
|
|
* 所以那边从来不出这个问题 —— 抄过来的时候**把渲染那一半丢了**。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⇒ 现在统一走这里:赋值 + **当场弹 Toast**。
|
|
|
|
|
|
* ⚠️ 为什么用 Toast 而不是页面里加一行 `Text`:本页是长滚动页,
|
|
|
|
|
|
* 用户常常是在页面中部/底部点按钮的,页面顶部那行提示**根本不在视口里**
|
|
|
|
|
|
* (pitfalls 343 就是这个:错误提示渲染在滚动区里 = 用户看不到)。
|
|
|
|
|
|
* Toast 浮在最上层,与滚动位置无关。
|
|
|
|
|
|
* ⚠️ `syncNote` 仍然保留赋值:它是**可断言的落点**(守卫与真机探针都靠它),
|
|
|
|
|
|
* 而且将来若要在页内显示历史提示,值还在。
|
|
|
|
|
|
*/
|
|
|
|
|
|
private note(msg: string): void {
|
|
|
|
|
|
this.syncNote = msg;
|
|
|
|
|
|
try {
|
|
|
|
|
|
this.getUIContext().getPromptAction().showToast({ message: msg, duration: 2600 });
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
hilog.warn(DOMAIN, TAG, '首页提示弹不出来:%{public}s', JSON.stringify(err));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 确保日历本已加载(v84 新建 / v86 编辑**共用**)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 🔴 为什么抽出来:v84 只有「新建」需要日程本,v86 加了「就地编辑」之后
|
|
|
|
|
|
* 编辑表单同样要 `books`(不然"日程本"那一栏是空的、保存时静默丢失归属)。
|
|
|
|
|
|
* 两处各写一遍懒加载 = 迟早一处改了另一处没改(本项目反复出现的形态)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* @returns true = 有可写的本,可以开表单;false = 已经给过提示,调用方直接 return
|
|
|
|
|
|
*/
|
|
|
|
|
|
private async ensureCalBooks(): Promise<boolean> {
|
2026-10-24 19:14:18 +08:00
|
|
|
|
const token: string = FB_TOKEN_STORE.get();
|
|
|
|
|
|
if (token === '') {
|
2026-10-24 20:22:35 +08:00
|
|
|
|
this.note('还没登录,无法操作日程');
|
|
|
|
|
|
return false;
|
2026-10-24 19:14:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
if (this.calBooks.length === 0) {
|
|
|
|
|
|
this.calLoadingBooks = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const list: FbCalBook[] = await FbCalRepo.inst().fetchBooks(token);
|
|
|
|
|
|
this.calBooks = list;
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
hilog.warn(DOMAIN, TAG, '首页拉日历本失败:%{public}s', JSON.stringify(err));
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
this.calLoadingBooks = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (this.calBooks.length === 0) {
|
2026-10-24 20:22:35 +08:00
|
|
|
|
/* ⚠️ 不要"打不开就算了":用户点了一下什么都没发生,是最难查的失败。
|
|
|
|
|
|
新建表单的"日程本"是必选项,没有可写本就画不出表单 ⇒ **明说一句**。 */
|
|
|
|
|
|
this.note('这个账号还没有可写的日程本,先去日程页设置里建一本');
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* v84:首页「今天的日程」区块点「+ 新建」→ **就地**打开新建表单。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ `presetDate` 用"今天":首页这条区块就叫「今天的日程」,
|
|
|
|
|
|
* 用户在这里点新建,十有八九是给今天加一件。
|
|
|
|
|
|
* ⚠️ 拉本期间用 `calLoadingBooks` 挡住连点(否则会开出两个表单)。
|
|
|
|
|
|
*/
|
|
|
|
|
|
private async openCalCreate(): Promise<void> {
|
|
|
|
|
|
if (this.calCreating || this.calEditing || this.calLoadingBooks) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!(await this.ensureCalBooks())) {
|
2026-10-24 19:14:18 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.calCreating = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-24 20:22:35 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* v86:日程**详情卡**上点「编辑」→ 就地打开编辑表单。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 🔴 用户原话(本轮):「端上也全部都实现。」上一轮我把它做成了"跳日程模块",
|
|
|
|
|
|
* 理由是"编辑是日程页的职责"。但那正是用户在 v73 明确否掉的行为
|
|
|
|
|
|
* (「点击之后都应该像那个日程里面的卡片一样弹出来,不要进入到日程里面」)。
|
|
|
|
|
|
* ⇒ 现在改成**就地编辑**:`FbEventEditSheet` 本来就是同一个组件
|
|
|
|
|
|
* (日程页用的也是它),首页复用它**不是"再写一份"**,没有漂移风险。
|
|
|
|
|
|
* ⚠️ 这与 v79 把任务的"跳编辑"改成"就地编辑"是同一个决定,理由也一样。
|
|
|
|
|
|
* ⚠️ 不能直接复用 `calCreating`:新建时 `event` 传 null、`isCreate: true`,
|
|
|
|
|
|
* `presetDate` 也要用;编辑时传 `evtFull`、`isCreate: false`。
|
|
|
|
|
|
* 两个状态位分开,语义才不糊(也才关得掉 —— `backOnce` 要按层判断)。
|
|
|
|
|
|
*/
|
|
|
|
|
|
private async openEventEdit(): Promise<void> {
|
|
|
|
|
|
if (this.evtFull === null || this.calEditing || this.calCreating) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!(await this.ensureCalBooks())) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.calEditing = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 16:40:44 +08:00
|
|
|
|
|
2026-10-21 20:01:37 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 把最新数据推给桌面上所有日程卡片。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 不 await、失败只记日志:这是"顺手做的一件好事",绝不能影响勾选动作。
|
|
|
|
|
|
*/
|
|
|
|
|
|
private pushCards(): void {
|
|
|
|
|
|
const ctx: common.Context | undefined = this.getUIContext().getHostContext();
|
|
|
|
|
|
if (ctx === undefined) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
FB_CARD_FORMS.pushAll(ctx).catch((err: BusinessError): void => {
|
|
|
|
|
|
hilog.warn(0x0000, 'FbHome', '刷新桌面卡片失败:%{public}s', err.message);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 日程模块主题色(详情卡环色 / 编辑按钮色) */
|
|
|
|
|
|
private calAccent(): string {
|
|
|
|
|
|
const m: FbModuleDef | undefined = fbModuleOf('calendar');
|
|
|
|
|
|
return m !== undefined ? fbThemeVars(m.color).brand : '#A84A28';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 任务模块主题色 */
|
|
|
|
|
|
private taskAccent(): string {
|
|
|
|
|
|
const m: FbModuleDef | undefined = fbModuleOf('tasks');
|
|
|
|
|
|
return m !== undefined ? fbThemeVars(m.color).brand : '#A8574F';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-24 20:22:35 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* v86:另外三个模块的主题色。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 三个方法长得一样却**不合成一个 `accentOf(key)`**:
|
|
|
|
|
|
* ArkUI 的 `@Builder`/方法里传字符串键很容易写错(`'movie'` vs `'movies'`),
|
|
|
|
|
|
* 而写错的后果是"表单标题色不对"—— 一个不会崩、只会有点怪的问题,
|
|
|
|
|
|
* 极难被发现。三个具体方法名让拼错在编译期就报错。
|
|
|
|
|
|
* ⚠️ 这与 `FbTaskEditSheet` 等组件只收 `accent: string` 的约定一致。
|
|
|
|
|
|
*/
|
|
|
|
|
|
private daysAccent(): string {
|
|
|
|
|
|
const m: FbModuleDef | undefined = fbModuleOf('days');
|
|
|
|
|
|
return m !== undefined ? fbThemeVars(m.color).brand : '#B4633C';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private moviesAccent(): string {
|
|
|
|
|
|
const m: FbModuleDef | undefined = fbModuleOf('movies');
|
|
|
|
|
|
return m !== undefined ? fbThemeVars(m.color).brand : '#6B7FA8';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private peopleAccent(): string {
|
|
|
|
|
|
const m: FbModuleDef | undefined = fbModuleOf('people');
|
|
|
|
|
|
return m !== undefined ? fbThemeVars(m.color).brand : '#4E7A63';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 名册里出现过的全部标签(新建成员时供点选)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 从**已加载的** `pfRecent` 推不出来 —— 它只有 6 个人。
|
|
|
|
|
|
* 这里直接调仓储的标签云(`tagCloud()`,本地库已算好并缓存)。
|
|
|
|
|
|
* ⚠️ 是**同步返回**:`tagCloud()` 是异步的,但 `@Param knownTags` 要立刻给值。
|
|
|
|
|
|
* ⇒ 传空数组也能用(用户可以手打标签),所以这里不做异步等待 ——
|
|
|
|
|
|
* 为了一个"可选的便利项"让表单打开晚 100ms 不值得。
|
|
|
|
|
|
*/
|
|
|
|
|
|
private personKnownTags(): string[] {
|
|
|
|
|
|
const out: string[] = [];
|
|
|
|
|
|
for (let i = 0; i < this.pfRecent.length; i++) {
|
|
|
|
|
|
const tags: string[] = this.pfRecent[i].tags;
|
|
|
|
|
|
for (let j = 0; j < tags.length; j++) {
|
|
|
|
|
|
if (out.indexOf(tags[j]) < 0) {
|
|
|
|
|
|
out.push(tags[j]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return out;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 只重读观影 / 联系人两张卡的本地库数据(新建成功后调,不动 `/home`) */
|
|
|
|
|
|
private reloadSideCards(): void {
|
|
|
|
|
|
this.loadSideCards();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-11 22:07:48 +08:00
|
|
|
|
/** 问候语时段(与 Web 端 home.php 里那段 PHP 同口径) */
|
|
|
|
|
|
private hello(): string {
|
|
|
|
|
|
const h: number = new Date().getHours();
|
|
|
|
|
|
if (h < 5) {
|
|
|
|
|
|
return '夜深了';
|
|
|
|
|
|
}
|
|
|
|
|
|
if (h < 11) {
|
|
|
|
|
|
return '早上好';
|
|
|
|
|
|
}
|
|
|
|
|
|
if (h < 13) {
|
|
|
|
|
|
return '中午好';
|
|
|
|
|
|
}
|
|
|
|
|
|
if (h < 18) {
|
|
|
|
|
|
return '下午好';
|
|
|
|
|
|
}
|
|
|
|
|
|
return '晚上好';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-18 03:35:39 +08:00
|
|
|
|
/** 日期行:`10 月 16 日 · 星期五`(服务端给的字段;没有就本地算兜底) */
|
2026-10-11 22:07:48 +08:00
|
|
|
|
private dateLine(): string {
|
2026-10-18 03:35:39 +08:00
|
|
|
|
if (this.todayLabel !== '') {
|
|
|
|
|
|
return this.weekday !== '' ? (this.todayLabel + ' · ' + this.weekday) : this.todayLabel;
|
|
|
|
|
|
}
|
2026-10-11 22:07:48 +08:00
|
|
|
|
const d: Date = new Date();
|
2026-10-18 03:35:39 +08:00
|
|
|
|
const w: string[] = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
|
|
|
|
|
|
return (d.getMonth() + 1).toString() + ' 月 ' + d.getDate().toString() + ' 日 · ' + w[d.getDay()];
|
2026-10-11 22:07:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-18 03:35:39 +08:00
|
|
|
|
/** 天气图标颜色(按服务端给的 tone 分档,与网页版 `.hb-weather.is-*` 一一对应) */
|
|
|
|
|
|
private weatherTint(): string {
|
|
|
|
|
|
const w: FbWeather | null = this.weather;
|
|
|
|
|
|
const tone: string = w !== null ? w.tone : 'cloud';
|
|
|
|
|
|
if (tone === 'sunny') {
|
|
|
|
|
|
return '#E8A33D';
|
|
|
|
|
|
}
|
|
|
|
|
|
if (tone === 'rain') {
|
|
|
|
|
|
return '#3B7FBF';
|
|
|
|
|
|
}
|
|
|
|
|
|
if (tone === 'snow') {
|
|
|
|
|
|
return '#5AA6B8';
|
|
|
|
|
|
}
|
|
|
|
|
|
if (tone === 'storm') {
|
|
|
|
|
|
return '#7A5BB5';
|
|
|
|
|
|
}
|
|
|
|
|
|
if (tone === 'fog') {
|
|
|
|
|
|
return '#8C8C8C';
|
|
|
|
|
|
}
|
|
|
|
|
|
return '#7C8AA0';
|
2026-10-11 22:07:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-18 03:35:39 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 日子标记的配色(与网页版 `.hb-mark.is-*` 同一套,抄自 `assets/css/app.css`)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 用**具体的背景/前景两色**而不是 `FbTag` 的 tone —— 网页版那五档
|
|
|
|
|
|
* (holiday 红 / term 绿 / festival 橙 / anniv 紫 / birthday 粉)
|
|
|
|
|
|
* 比 FbTag 现有的四档更细,硬套会丢掉"生日和纪念日不是一回事"这个区分。
|
|
|
|
|
|
*/
|
|
|
|
|
|
private markBg(kind: string): string {
|
|
|
|
|
|
if (kind === 'holiday') {
|
|
|
|
|
|
return '#FDECEC';
|
|
|
|
|
|
}
|
|
|
|
|
|
if (kind === 'term') {
|
|
|
|
|
|
return '#EDF4E9';
|
|
|
|
|
|
}
|
|
|
|
|
|
if (kind === 'festival') {
|
|
|
|
|
|
return '#FDF2E3';
|
|
|
|
|
|
}
|
|
|
|
|
|
if (kind === 'birthday') {
|
|
|
|
|
|
return '#FDECF3';
|
|
|
|
|
|
}
|
|
|
|
|
|
return '#F2ECF7'; // anniv 及其它
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private markFg(kind: string): string {
|
|
|
|
|
|
if (kind === 'holiday') {
|
|
|
|
|
|
return '#A8232B';
|
|
|
|
|
|
}
|
|
|
|
|
|
if (kind === 'term') {
|
|
|
|
|
|
return '#3F6B34';
|
|
|
|
|
|
}
|
|
|
|
|
|
if (kind === 'festival') {
|
|
|
|
|
|
return '#96551A';
|
|
|
|
|
|
}
|
|
|
|
|
|
if (kind === 'birthday') {
|
|
|
|
|
|
return '#A03060';
|
|
|
|
|
|
}
|
|
|
|
|
|
return '#6B4A86'; // anniv 及其它
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 当前时刻(当天第几分钟)—— 时间轴画"当前时间红线"用 */
|
|
|
|
|
|
private nowMinutes(): number {
|
|
|
|
|
|
const d: Date = new Date();
|
|
|
|
|
|
return d.getHours() * 60 + d.getMinutes();
|
2026-10-11 22:07:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-24 19:14:18 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 今天 → `YYYY-MM-DD`(`FbEventEditSheet` 的 `presetDate` 要这个格式)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ **不要**用 `toISOString().slice(0,10)`:那是 **UTC**,
|
|
|
|
|
|
* 东八区早上 8 点前会算成"昨天"(本项目真机上踩过同类:新建的日程
|
|
|
|
|
|
* 跑到前一天去了,用户完全无法理解)。必须用本地年月日拼。
|
|
|
|
|
|
* ⚠️ 月份、日期都要补零,否则 `2026-1-5` 服务端解析不了。
|
|
|
|
|
|
*/
|
|
|
|
|
|
private todayIso(): string {
|
|
|
|
|
|
const d: Date = new Date();
|
|
|
|
|
|
const m: number = d.getMonth() + 1;
|
|
|
|
|
|
const day: number = d.getDate();
|
|
|
|
|
|
return d.getFullYear().toString() + '-'
|
|
|
|
|
|
+ (m < 10 ? '0' + m.toString() : m.toString()) + '-'
|
|
|
|
|
|
+ (day < 10 ? '0' + day.toString() : day.toString());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-11 22:07:48 +08:00
|
|
|
|
build() {
|
2026-10-21 20:01:37 +08:00
|
|
|
|
/* 🔴 v73:外层必须是 **Stack**(叠放),不能是 Column(排列)。
|
|
|
|
|
|
浮层要"盖在页面上",而不是"排在页面后面" —— 后者会把 Scroll 挤出屏幕,
|
|
|
|
|
|
表现是"点了日程详情打不开 / 打开了但整页空了"。
|
|
|
|
|
|
(本项目 2026-10-20 真机事故就是这个:`Column { 整高遮罩; 卡片 }`
|
|
|
|
|
|
把卡片挤出可视区,编译通过、`aboutToAppear` 照常跑、`dumpLayout` 里一个节点都没有。)
|
|
|
|
|
|
⚠️ 这也是为什么**根容器是 Stack 而不是 Scroll**:遮罩与卡片必须是它的兄弟,
|
|
|
|
|
|
不能塞进 Scroll 的 Column 里。 */
|
|
|
|
|
|
Stack({ alignContent: Alignment.TopStart }) {
|
2026-11-05 13:43:03 +08:00
|
|
|
|
/* #59:绑 Scroller —— 概述四格数字点击后要滚到对应区块 */
|
|
|
|
|
|
Scroll(this.homeScroller) {
|
2026-10-21 20:01:37 +08:00
|
|
|
|
Column({ space: 0 }) {
|
|
|
|
|
|
this.topBar()
|
2026-10-12 10:06:55 +08:00
|
|
|
|
|
2026-10-12 08:43:32 +08:00
|
|
|
|
// ==================== ① 个人信息卡(问候 + 今天 + 天气)====================
|
|
|
|
|
|
// 🔴 用户 2026-10-12 定的:这一块**要白色卡片**("这样显得好看")。
|
|
|
|
|
|
// ⚠️ 曾一度做成"不套卡片的页面头部",理由是省首屏高度;
|
|
|
|
|
|
// 用户看了实机后仍要求套白卡 ⇒ **以用户观感为准**,别再改回去。
|
|
|
|
|
|
// 注意与顶栏的区别:顶栏**不能**有自己的背景色(否则与状态栏割裂),
|
|
|
|
|
|
// 而这张卡在顶栏**下面**、与状态栏不相邻,套白卡没有这个问题。
|
2026-10-11 23:20:49 +08:00
|
|
|
|
Column({ space: 10 }) {
|
|
|
|
|
|
// 第一行:问候 + 名字(左) / 温度(右)
|
2026-10-11 22:07:48 +08:00
|
|
|
|
Row() {
|
2026-10-11 23:20:49 +08:00
|
|
|
|
Column({ space: 5 }) {
|
2026-10-11 22:07:48 +08:00
|
|
|
|
Row({ space: 4 }) {
|
|
|
|
|
|
Text(this.hello())
|
2026-10-11 23:20:49 +08:00
|
|
|
|
.fontSize(22)
|
2026-10-11 22:07:48 +08:00
|
|
|
|
.fontWeight(FontWeight.Medium)
|
|
|
|
|
|
.fontColor(FbColors.ink)
|
|
|
|
|
|
Text(',' + this.app.session.me.name)
|
2026-10-11 23:20:49 +08:00
|
|
|
|
.fontSize(22)
|
|
|
|
|
|
.fontWeight(FontWeight.Medium)
|
|
|
|
|
|
.fontColor(FbColors.ink)
|
2026-10-11 22:07:48 +08:00
|
|
|
|
}
|
2026-10-11 23:20:49 +08:00
|
|
|
|
.alignItems(VerticalAlign.Bottom)
|
|
|
|
|
|
|
|
|
|
|
|
// 日期 · 星期 · 农历(一行排开,手机上一行读得完)
|
2026-10-11 22:07:48 +08:00
|
|
|
|
Row({ space: 8 }) {
|
|
|
|
|
|
Text(this.dateLine())
|
|
|
|
|
|
.fontSize(FbMetric.fontSmall)
|
2026-10-11 23:20:49 +08:00
|
|
|
|
.fontColor(FbColors.muted)
|
2026-10-18 03:35:39 +08:00
|
|
|
|
// 🔴 农历:真实值(接口给)。算不出时**不显示**,不编一个
|
|
|
|
|
|
if (this.lunarShort !== '') {
|
|
|
|
|
|
Text(this.lunarShort)
|
|
|
|
|
|
.fontSize(FbMetric.fontSmall)
|
|
|
|
|
|
.fontColor(FbColors.faint)
|
|
|
|
|
|
}
|
2026-10-11 22:07:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
.alignItems(HorizontalAlign.Start)
|
|
|
|
|
|
.layoutWeight(1)
|
|
|
|
|
|
|
2026-10-11 23:20:49 +08:00
|
|
|
|
// 天气:靠右,温度大字 + 文案小字
|
2026-10-18 03:35:39 +08:00
|
|
|
|
// ⚠️ 没配地点 / 取不到天气时**整块不画**(与网页版一致)——
|
|
|
|
|
|
// 画一个"——"占位反而让人以为坏了。
|
|
|
|
|
|
if (this.weather !== null) {
|
|
|
|
|
|
Row({ space: 6 }) {
|
|
|
|
|
|
Text(this.weather!.icon)
|
|
|
|
|
|
.fontSize(22)
|
|
|
|
|
|
.fontColor(this.weatherTint())
|
|
|
|
|
|
Column({ space: 0 }) {
|
|
|
|
|
|
Text(this.weather!.temp)
|
|
|
|
|
|
.fontSize(24)
|
|
|
|
|
|
.fontWeight(FontWeight.Medium)
|
|
|
|
|
|
.fontColor(FbColors.ink)
|
|
|
|
|
|
Text(this.weather!.text)
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(FbColors.muted)
|
|
|
|
|
|
.maxLines(1)
|
|
|
|
|
|
}
|
|
|
|
|
|
.alignItems(HorizontalAlign.End)
|
2026-10-11 22:07:48 +08:00
|
|
|
|
}
|
2026-10-18 03:35:39 +08:00
|
|
|
|
.alignItems(VerticalAlign.Center)
|
2026-10-11 22:07:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.alignItems(VerticalAlign.Top)
|
|
|
|
|
|
|
2026-10-18 03:35:39 +08:00
|
|
|
|
// 第二行:天气地点 + 日子标记(节气 / 节日 / 纪念日 / 生日)
|
|
|
|
|
|
// 🔴🔴 这里**绝不能**再出现 `FB_DOCK_RESERVE` 的 padding ——
|
|
|
|
|
|
// 它 96vp,接在这会把这个小方块撑成一大片空白(v54 修的那个"概述块太高")。
|
2026-10-11 23:20:49 +08:00
|
|
|
|
Column({ space: 6 }) {
|
2026-10-18 03:35:39 +08:00
|
|
|
|
if (this.weather !== null && (this.weather!.place !== '' || this.weather!.wind !== '')) {
|
|
|
|
|
|
Text(this.weatherLine())
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(FbColors.faint)
|
|
|
|
|
|
.maxLines(1)
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 日子标记:今天是什么节气/节日/纪念日/谁的生日(真实数据)
|
|
|
|
|
|
if (this.marks.length > 0) {
|
|
|
|
|
|
Flex({ wrap: FlexWrap.Wrap }) {
|
|
|
|
|
|
ForEach(this.marks, (mk: FbMark, idx: number) => {
|
|
|
|
|
|
Text(mk.word)
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(this.markFg(mk.kind))
|
|
|
|
|
|
.backgroundColor(this.markBg(mk.kind))
|
|
|
|
|
|
.borderRadius(FbMetric.radiusPill)
|
|
|
|
|
|
.padding({ left: 9, right: 9, top: 2, bottom: 2 })
|
|
|
|
|
|
.margin({ right: 6, top: 2 })
|
|
|
|
|
|
}, (mk: FbMark, idx: number): string => 'mk_' + idx.toString() + '_' + mk.word)
|
|
|
|
|
|
}
|
2026-10-11 23:20:49 +08:00
|
|
|
|
.width('100%')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.alignItems(HorizontalAlign.Start)
|
|
|
|
|
|
|
2026-10-18 03:35:39 +08:00
|
|
|
|
// 第三行:四个数字(**与网页版 `hb-stats` 同源**)
|
|
|
|
|
|
// `N 条日程(今天)· N 件在做 · N 件待办 · N 件逾期`
|
|
|
|
|
|
// 🔴 用户 2026-10-16:「下面……**未来7天**……这几个就是去掉了,不要了。」
|
|
|
|
|
|
// ⇒ 原来这里有"未来七天"那一格(today..+7 天的条数),已去掉;
|
|
|
|
|
|
// 换成与网页版一样的口径(网页版概述卡本来也是这四个)。
|
|
|
|
|
|
//
|
|
|
|
|
|
// 🔴🔴 v54b(用户 2026-10-17):「里面的**日程、在做、待办的数量是错误的**」
|
|
|
|
|
|
// 真机实测:接口与端上缓存里都是 `{today_events:1, doing:3, todo:2}`(正确),
|
|
|
|
|
|
// 而界面显示 `0 / 0 / 0` —— 同一份数据里「今天的日程 1 条」却是对的。
|
|
|
|
|
|
// 根因:原来是 `FbStatTriple`,它内部用
|
|
|
|
|
|
// `@Builder cell(label, value, alarm)` —— **多值参数**,
|
|
|
|
|
|
// SDK 规则明写「**两个及以上参数不触发刷新**」
|
|
|
|
|
|
// ⇒ 初始渲染的 0 永远不会被后来的真实值替换。
|
|
|
|
|
|
// (本项目第 6 次踩同一个坑:@Builder 值参数不参与状态观察。)
|
|
|
|
|
|
// ⇒ 抽成 **`FbStatCell` 组件**(@ComponentV2 + @Param,@Param 参与观察)
|
|
|
|
|
|
// —— 与 `FbEventChip` / `FbCalOptionRow` / `FbTagPickChip` 同一套修法。
|
|
|
|
|
|
// ⚠️ 不要改回 `@Builder cell(...)`;也不要用**本页的 @Builder**(同样是按值传递)。
|
2026-10-12 08:43:32 +08:00
|
|
|
|
Divider().color(FbColors.line2)
|
2026-10-18 03:35:39 +08:00
|
|
|
|
Row() {
|
2026-11-05 13:43:03 +08:00
|
|
|
|
/* #59:四格都可点,滚到对应区块。
|
|
|
|
|
|
⚠️ 「在做 / 任务 / 逾期」都指向「近期任务」——
|
|
|
|
|
|
它们都是任务的不同状态计数,点哪个都是想去看任务。
|
|
|
|
|
|
让"逾期"单独跳一个不存在的"逾期列表"反而是骗人的。 */
|
|
|
|
|
|
FbStatCell({
|
|
|
|
|
|
value: this.statToday,
|
|
|
|
|
|
label: '今天日程',
|
|
|
|
|
|
onTap: (): void => {
|
|
|
|
|
|
this.jumpToEvents();
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
FbStatCell({
|
|
|
|
|
|
value: this.statDoing,
|
|
|
|
|
|
label: '在做',
|
|
|
|
|
|
onTap: (): void => {
|
|
|
|
|
|
this.jumpToTasks();
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
FbStatCell({
|
|
|
|
|
|
value: this.statTodo,
|
|
|
|
|
|
label: '任务',
|
|
|
|
|
|
onTap: (): void => {
|
|
|
|
|
|
this.jumpToTasks();
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
FbStatCell({
|
|
|
|
|
|
value: this.statOverdue,
|
|
|
|
|
|
label: '逾期',
|
|
|
|
|
|
alarm: true,
|
|
|
|
|
|
onTap: (): void => {
|
|
|
|
|
|
this.jumpToTasks();
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
2026-10-18 03:35:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding({ top: 10, bottom: 10 })
|
2026-10-11 22:07:48 +08:00
|
|
|
|
}
|
2026-10-12 11:37:45 +08:00
|
|
|
|
// 🔴 不要 `.width('100%')` —— 它会先把宽度占满,导致后面的 margin 失效,
|
|
|
|
|
|
// 卡片于是**顶到屏幕两侧边缘**(实测 x=0..1080),而别的区块是留边 16vp 的,
|
|
|
|
|
|
// 页面里就出现"有的满宽、有的有缝"的不统一(用户 2026-10-12 的反馈)。
|
|
|
|
|
|
// 正确做法:**只给 margin**,宽度由 margin 自动撑出(左右各 16vp)。
|
2026-10-12 08:43:32 +08:00
|
|
|
|
.padding(FbMetric.cardPad)
|
2026-10-12 11:37:45 +08:00
|
|
|
|
// 白色卡片:与下面「今天的日程」等块同一种卡片语言
|
2026-10-12 08:43:32 +08:00
|
|
|
|
.backgroundColor(FbColors.surface)
|
|
|
|
|
|
.borderRadius(FbMetric.radius)
|
|
|
|
|
|
.margin({
|
2026-10-11 23:20:49 +08:00
|
|
|
|
left: FbMetric.pagePad,
|
|
|
|
|
|
right: FbMetric.pagePad,
|
2026-10-12 08:43:32 +08:00
|
|
|
|
top: 8
|
2026-10-11 23:20:49 +08:00
|
|
|
|
})
|
|
|
|
|
|
.alignItems(HorizontalAlign.Start)
|
2026-10-11 22:07:48 +08:00
|
|
|
|
|
2026-10-18 03:35:39 +08:00
|
|
|
|
// ==================== ② 今天的日程 ====================
|
|
|
|
|
|
// 🔴 用户 2026-10-16:「今天的日程,现在显示的是日程里面的展示内容。
|
|
|
|
|
|
// 把**今天那个块**拿到首页上来就行了。」
|
|
|
|
|
|
// ⇒ 直接用日程页那块面板组件(月/周/列表三处用的同一个)——
|
|
|
|
|
|
// 于是显示方式(时间轴/列表)、冲突横向并排、当前时间红线
|
|
|
|
|
|
// 在首页与日程页**完全一致**,不会再出现"两个地方长得不一样"。
|
|
|
|
|
|
// ⚠️ 面板自带卡片样式与外边距,这里**不要再套一层卡片**。
|
|
|
|
|
|
// ⚠️ `showHead: false` —— 本页已有 `FbSectionTitle`,两个头会重复。
|
|
|
|
|
|
// ⚠️ 空态文案也换掉:首页没有"右下角 +"那个按钮。
|
2026-10-24 19:14:18 +08:00
|
|
|
|
FbSectionTitle({
|
|
|
|
|
|
title: '今天的日程',
|
2026-11-05 15:52:40 +08:00
|
|
|
|
accent: this.calAccent(),
|
2026-10-24 19:14:18 +08:00
|
|
|
|
extra: this.todayEvents.length.toString() + ' 条',
|
|
|
|
|
|
/* 🔴 v84(用户 2026-10-23):「日程,没有新建日程按钮」。
|
|
|
|
|
|
⚠️ 别再删掉这颗按钮 —— 它是本条的**唯一可见证据**,
|
|
|
|
|
|
删了用户立刻又说"没有按钮"(上一轮就漏在端上)。 */
|
2026-11-05 15:52:40 +08:00
|
|
|
|
addText: '新建',
|
2026-10-24 19:14:18 +08:00
|
|
|
|
onAdd: (): void => {
|
|
|
|
|
|
this.openCalCreate();
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
2026-10-18 03:35:39 +08:00
|
|
|
|
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12 })
|
|
|
|
|
|
|
2026-11-05 13:43:03 +08:00
|
|
|
|
/* #59:记录「今天的日程」区块的 y 偏移(概述格「今天日程」点击要滚到这里) */
|
2026-10-18 03:35:39 +08:00
|
|
|
|
FbDayEventPanel({
|
|
|
|
|
|
label: this.todayEvents.length > 0 ? '今天' : '',
|
|
|
|
|
|
events: this.todayEvents,
|
|
|
|
|
|
style: this.calStyle,
|
|
|
|
|
|
isToday: true,
|
|
|
|
|
|
nowMin: this.nowMinutes(),
|
|
|
|
|
|
showHead: false,
|
|
|
|
|
|
emptyTitle: '今天没有安排',
|
|
|
|
|
|
emptyHint: '过得轻松一点',
|
|
|
|
|
|
onPick: (e: FbCalEvent): void => {
|
2026-10-21 20:01:37 +08:00
|
|
|
|
/* 🔴 v73(用户 2026-10-21):**就地弹详情卡**,不跳模块。
|
|
|
|
|
|
旧实现是 `this.app.switchModule('calendar')` —— 点哪条都跳到日程页,
|
|
|
|
|
|
用户还得自己再找一遍那条(用户明确否掉了这个行为)。 */
|
|
|
|
|
|
this.openEvent(e);
|
2026-10-18 03:35:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
})
|
2026-11-05 13:43:03 +08:00
|
|
|
|
/* #59:记录 y 偏移 —— 概述格「今天日程」点击要滚到这里。
|
|
|
|
|
|
⚠️ onAreaChange 在首次布局和每次内容变化时都触发,
|
|
|
|
|
|
所以坐标持续有效(任务数变化会让下面的区块位移)。 */
|
|
|
|
|
|
.onAreaChange((_o: Area, n: Area): void => {
|
|
|
|
|
|
this.yEvents = Number(n.position.y);
|
|
|
|
|
|
})
|
2026-10-18 03:35:39 +08:00
|
|
|
|
|
|
|
|
|
|
// ==================== ③ 近期任务 ====================
|
2026-10-11 22:07:48 +08:00
|
|
|
|
Column({ space: 0 }) {
|
2026-10-24 20:22:35 +08:00
|
|
|
|
FbSectionTitle({
|
|
|
|
|
|
title: '近期任务',
|
2026-11-05 15:52:40 +08:00
|
|
|
|
accent: this.taskAccent(),
|
2026-10-24 20:22:35 +08:00
|
|
|
|
extra: this.taskExtra(),
|
|
|
|
|
|
/* 🔴 v86(用户「端上也全部都实现」):与 Web 端 v78 对齐,卡片上直接新建。
|
|
|
|
|
|
⚠️ 别再删掉这颗按钮 —— 与日程那颗同理,它是本条需求的**唯一可见证据**。 */
|
2026-11-05 15:52:40 +08:00
|
|
|
|
addText: '新建',
|
2026-10-24 20:22:35 +08:00
|
|
|
|
onAdd: (): void => {
|
|
|
|
|
|
this.openTaskCreate();
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
2026-10-18 03:35:39 +08:00
|
|
|
|
Column({ space: 0 }) {
|
|
|
|
|
|
if (this.tasks.length === 0) {
|
|
|
|
|
|
Row() {
|
2026-11-05 12:41:04 +08:00
|
|
|
|
Text('没有进行中的任务,也没有待做的任务。')
|
2026-10-18 03:35:39 +08:00
|
|
|
|
.fontSize(FbMetric.fontSmall)
|
|
|
|
|
|
.fontColor(FbColors.muted)
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding({ top: 12, bottom: 12 })
|
2026-10-11 22:07:48 +08:00
|
|
|
|
} else {
|
2026-10-18 03:35:39 +08:00
|
|
|
|
ForEach(this.tasks, (t: FbTaskItem, idx: number) => {
|
|
|
|
|
|
this.taskRow(t, idx === this.tasks.length - 1)
|
2026-10-22 17:33:32 +08:00
|
|
|
|
/* 🔴 v79:键必须带状态等**会被画到行上的字段**(`taskKey` 的注释有完整推导)。
|
|
|
|
|
|
只用 id 的后果:面板里改成已完成 → 重拉 → 对象换了但键没变
|
|
|
|
|
|
⇒ ArkUI 复用旧节点 ⇒ 首页这张卡还是"待办"。 */
|
|
|
|
|
|
}, (t: FbTaskItem, idx: number): string => taskKey(t))
|
2026-10-11 22:07:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
2026-10-18 03:35:39 +08:00
|
|
|
|
.padding({ left: FbMetric.cardPad, right: FbMetric.cardPad, top: 4, bottom: 6 })
|
2026-10-11 22:07:48 +08:00
|
|
|
|
.backgroundColor(FbColors.surface)
|
|
|
|
|
|
.borderRadius(FbMetric.radius)
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
2026-10-18 03:35:39 +08:00
|
|
|
|
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12 })
|
2026-11-05 13:43:03 +08:00
|
|
|
|
/* #59:记录「近期任务」区块的 y —— 概述格「在做/任务/逾期」点击滚到这里。
|
|
|
|
|
|
⚠️ 挂在区块最外层 Column 上(含标题+内容),滚到的是整块的开头。 */
|
|
|
|
|
|
.onAreaChange((_o: Area, n: Area): void => {
|
|
|
|
|
|
this.yTasks = Number(n.position.y);
|
|
|
|
|
|
})
|
2026-10-11 22:07:48 +08:00
|
|
|
|
|
2026-10-18 20:14:40 +08:00
|
|
|
|
// ==================== ④ 最近纪念日 ====================
|
|
|
|
|
|
// 🔴🔴 v59(用户 2026-10-18):
|
|
|
|
|
|
// 「首页应该加一个『最近纪念日』模块,放在『往年的今天』前面、
|
|
|
|
|
|
// 『近期任务』后面。内容包括:
|
|
|
|
|
|
// 1. 未来最近的三个纪念日,包括每个人的生日,以及手动填写的各个纪念日。
|
|
|
|
|
|
// 2. 以倒计时形式展示:距离哪个纪念日还有多少天,
|
|
|
|
|
|
// 距离谁谁谁的生日还有多少天。」
|
|
|
|
|
|
// ⇒ 位置就卡在这两段之间(**不是**"放在首页某处")。
|
|
|
|
|
|
// ⇒ 数据来自 `/home` 的 `recent_days`(服务端已排好序、最多 3 条),
|
|
|
|
|
|
// 来源是"派生生日 + 自建纪念日",正好覆盖用户说的两样。
|
|
|
|
|
|
Column({ space: 0 }) {
|
|
|
|
|
|
FbSectionTitle({
|
|
|
|
|
|
title: '最近纪念日',
|
2026-11-05 15:52:40 +08:00
|
|
|
|
accent: this.daysAccent(),
|
2026-10-18 20:14:40 +08:00
|
|
|
|
extra: this.recentDays.length > 0
|
|
|
|
|
|
? this.recentDays.length.toString() + ' 个'
|
2026-10-24 20:22:35 +08:00
|
|
|
|
: '',
|
|
|
|
|
|
/* 🔴 v86(用户「端上也全部都实现」):与 Web 端 v81 对齐 ——
|
|
|
|
|
|
「如果是纪念日,就有一个『新增纪念日』」。
|
|
|
|
|
|
⚠️ 空态文案原来写「在『纪念日』里添一个」,现在卡片上就能添,
|
|
|
|
|
|
文案改成指这颗按钮(否则用户会以为必须进模块)。 */
|
2026-11-05 15:52:40 +08:00
|
|
|
|
addText: '新建',
|
2026-10-24 20:22:35 +08:00
|
|
|
|
onAdd: (): void => {
|
|
|
|
|
|
this.openDayCreate();
|
|
|
|
|
|
}
|
2026-10-18 20:14:40 +08:00
|
|
|
|
})
|
|
|
|
|
|
Column({ space: 0 }) {
|
|
|
|
|
|
if (this.recentDays.length === 0) {
|
|
|
|
|
|
Column({ space: 6 }) {
|
|
|
|
|
|
Text('还没有要过的日子')
|
|
|
|
|
|
.fontSize(FbMetric.fontSmall)
|
|
|
|
|
|
.fontColor(FbColors.ink)
|
2026-11-05 15:52:40 +08:00
|
|
|
|
Text('点右上角「新建」添一个,或者给家人的资料里填上生日。')
|
2026-10-18 20:14:40 +08:00
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(FbColors.muted)
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.alignItems(HorizontalAlign.Start)
|
|
|
|
|
|
.padding({ top: 12, bottom: 12 })
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ForEach(this.recentDays, (d: FbRecentDay, idx: number) => {
|
|
|
|
|
|
this.recentDayRow(d, idx === this.recentDays.length - 1)
|
|
|
|
|
|
}, (d: FbRecentDay, idx: number): string =>
|
|
|
|
|
|
'rd_' + d.date + '_' + d.title + '_' + idx.toString())
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding({ left: FbMetric.cardPad, right: FbMetric.cardPad, top: 4, bottom: 6 })
|
|
|
|
|
|
.backgroundColor(FbColors.surface)
|
|
|
|
|
|
.borderRadius(FbMetric.radius)
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12 })
|
|
|
|
|
|
|
2026-10-24 20:22:35 +08:00
|
|
|
|
// ==================== ⑤ 最近观影(v86 新增)====================
|
|
|
|
|
|
/* 🔴 用户 2026-10-24「端上也全部都实现」。
|
|
|
|
|
|
Web 端首页一直有这张卡(`_card_movies.php`,v62 就有,v81 补了「新增电影」),
|
|
|
|
|
|
端上首页**从来没有**过 ⇒ 用户在手机上完全看不到观影的近期动态。
|
|
|
|
|
|
⇒ 位置放在「最近纪念日」之后、「往年的今天」之前:
|
|
|
|
|
|
上面三块是"今天/最近要做的事",下面两块是"往回看的日子",
|
|
|
|
|
|
观影正好是两者的交界(刚看过的片子既是记录、也进"往年的今天")。
|
|
|
|
|
|
|
|
|
|
|
|
⚠️ 数据走端上**本地库**(`FbMovieRepo.list()`),不是 `/home`:
|
|
|
|
|
|
`/home` 不带观影列表,而本地库由 `syncAll` 定期同步 —— 与电影页
|
|
|
|
|
|
同一个数据源,不会出现"首页和电影页不一致"。
|
|
|
|
|
|
⚠️ 卡片上只放 4 条、只显示「片名 + 日期 + 影院」:
|
|
|
|
|
|
海报要看图、要占一大块,卡片上不值得(点进电影页再看)。 */
|
|
|
|
|
|
Column({ space: 0 }) {
|
|
|
|
|
|
FbSectionTitle({
|
|
|
|
|
|
title: '最近观影',
|
2026-11-05 15:52:40 +08:00
|
|
|
|
accent: this.moviesAccent(),
|
2026-10-24 20:22:35 +08:00
|
|
|
|
extra: this.mvRecent.length > 0
|
|
|
|
|
|
? this.mvRecent.length.toString() + ' 部'
|
|
|
|
|
|
: '',
|
|
|
|
|
|
/* 文案照用户原话用「新增电影」—— 与 Web 端 `HomeCards` 的 `add` 一字不差。
|
|
|
|
|
|
⚠️ 表单标题仍是模块里的「记一场」,不为了统一去改模块自己的措辞。 */
|
2026-11-05 15:52:40 +08:00
|
|
|
|
addText: '新增',
|
2026-10-24 20:22:35 +08:00
|
|
|
|
onAdd: (): void => {
|
|
|
|
|
|
this.openMovieCreate();
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
Column({ space: 0 }) {
|
|
|
|
|
|
if (this.mvRecent.length === 0) {
|
|
|
|
|
|
Column({ space: 6 }) {
|
|
|
|
|
|
Text(this.mvLoading ? '正在读观影记录…' : '还没有观影记录')
|
|
|
|
|
|
.fontSize(FbMetric.fontSmall)
|
|
|
|
|
|
.fontColor(FbColors.ink)
|
2026-11-05 15:52:40 +08:00
|
|
|
|
Text('点右上角「新增」记一场,看过的片子都会留在这里。')
|
2026-10-24 20:22:35 +08:00
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(FbColors.muted)
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.alignItems(HorizontalAlign.Start)
|
|
|
|
|
|
.padding({ top: 12, bottom: 12 })
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ForEach(this.mvRecent, (m: FbMovieListItem, idx: number) => {
|
|
|
|
|
|
this.movieRow(m, idx === this.mvRecent.length - 1)
|
|
|
|
|
|
}, (m: FbMovieListItem, idx: number): string => 'mv_' + m.id.toString())
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding({ left: FbMetric.cardPad, right: FbMetric.cardPad, top: 4, bottom: 6 })
|
|
|
|
|
|
.backgroundColor(FbColors.surface)
|
|
|
|
|
|
.borderRadius(FbMetric.radius)
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12 })
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== ⑥ 联系人(v86 新增)====================
|
|
|
|
|
|
/* 🔴 同一条用户需求(「端上也全部都实现」)。
|
|
|
|
|
|
Web 端 v62 就有这张卡、v82 补了「新增成员」;端上首页一直没有。
|
|
|
|
|
|
⚠️ 只列**我的名册**(`FbPersonRepo.list()`),不是全家所有人 ——
|
|
|
|
|
|
首页是"我的"(与 Web 端 `contactsData($familyId, $me, $n)` 同口径)。
|
|
|
|
|
|
⚠️ 头像用「首字 + 底色」而不是真图:卡片上一个个去拉头像会打一堆请求,
|
|
|
|
|
|
而且名册里大部分人是没传头像的(实测线上 1089 人里绝大多数是首字)。
|
|
|
|
|
|
有真头像时仍优先用真图 —— 见 `personAvatar()`。 */
|
|
|
|
|
|
Column({ space: 0 }) {
|
|
|
|
|
|
FbSectionTitle({
|
|
|
|
|
|
title: '联系人',
|
2026-11-05 15:52:40 +08:00
|
|
|
|
accent: this.peopleAccent(),
|
2026-10-24 20:22:35 +08:00
|
|
|
|
extra: this.pfTotal > 0 ? this.pfTotal.toString() + ' 人' : '',
|
2026-11-05 15:52:40 +08:00
|
|
|
|
addText: '新增',
|
2026-10-24 20:22:35 +08:00
|
|
|
|
onAdd: (): void => {
|
|
|
|
|
|
this.openPersonCreate();
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
Column({ space: 0 }) {
|
|
|
|
|
|
if (this.pfRecent.length === 0) {
|
|
|
|
|
|
Column({ space: 6 }) {
|
|
|
|
|
|
Text(this.pfLoading ? '正在读名册…' : '名册里还没有人')
|
|
|
|
|
|
.fontSize(FbMetric.fontSmall)
|
|
|
|
|
|
.fontColor(FbColors.ink)
|
2026-11-05 15:52:40 +08:00
|
|
|
|
Text('点右上角「新增」把家人加进来。')
|
2026-10-24 20:22:35 +08:00
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(FbColors.muted)
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.alignItems(HorizontalAlign.Start)
|
|
|
|
|
|
.padding({ top: 12, bottom: 12 })
|
|
|
|
|
|
} else {
|
|
|
|
|
|
/* 三列网格:6 个人正好两行,手机上一屏内不压其他卡。
|
|
|
|
|
|
⚠️ 用 `Flex(wrap)` 而不是 `Row` —— `Row` 不换行,
|
|
|
|
|
|
超过 3 个会横向溢出到卡片外面(本项目 v76 的著名坑)。 */
|
|
|
|
|
|
Flex({ wrap: FlexWrap.Wrap }) {
|
|
|
|
|
|
ForEach(this.pfRecent, (p: FbRosterPerson) => {
|
|
|
|
|
|
this.personTile(p)
|
|
|
|
|
|
}, (p: FbRosterPerson): string => 'pf_' + p.id.toString())
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding({ top: 10, bottom: 2 })
|
|
|
|
|
|
|
|
|
|
|
|
if (this.pfTotal > this.pfRecent.length) {
|
|
|
|
|
|
Text('共 ' + this.pfTotal.toString() + ' 人,这里显示 '
|
|
|
|
|
|
+ this.pfRecent.length.toString() + ' 位。')
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(FbColors.muted)
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding({ top: 6, bottom: 4 })
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding({ left: FbMetric.cardPad, right: FbMetric.cardPad, top: 4, bottom: 6 })
|
|
|
|
|
|
.backgroundColor(FbColors.surface)
|
|
|
|
|
|
.borderRadius(FbMetric.radius)
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12 })
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== ⑦ 往年的今天 ====================
|
2026-10-18 03:35:39 +08:00
|
|
|
|
// 🔴 用户 2026-10-17:「近期任务的下面,应该加上**往年的今天**,
|
|
|
|
|
|
// 跟电脑端一样,你看看那个是什么逻辑。」
|
|
|
|
|
|
// ⇒ 位置**就在近期任务下面**(用户明确指定,不是"放在首页某处")。
|
2026-10-18 20:14:40 +08:00
|
|
|
|
// ⚠️ v59 起它上面多了一块「最近纪念日」(用户 2026-10-18 指定插在
|
|
|
|
|
|
// 「近期任务之后、往年的今天之前」)—— 两块都是"日子"类信息,
|
|
|
|
|
|
// 顺序是用户定的,别再调。
|
2026-10-18 03:35:39 +08:00
|
|
|
|
// 电脑端的逻辑见 `App\Home::memories()`:只认**往年的同一天**
|
|
|
|
|
|
// (不退回同月),来源是「看过的电影」+「走过的**实际行程**」,
|
|
|
|
|
|
// 合并按时间倒序取 6 条。
|
|
|
|
|
|
// ⚠️ 数据来自 `/home` 的 `memories` —— 与网页版**同一份实现**,
|
|
|
|
|
|
// 不要端上自己再查一遍(会出现"网页 2 年前、手机 3 年前")。
|
2026-10-11 22:07:48 +08:00
|
|
|
|
Column({ space: 0 }) {
|
|
|
|
|
|
FbSectionTitle({
|
2026-10-18 03:35:39 +08:00
|
|
|
|
title: '往年的今天',
|
|
|
|
|
|
extra: this.todayLabel !== '' ? this.todayLabel : ''
|
2026-10-11 22:07:48 +08:00
|
|
|
|
})
|
2026-10-18 03:35:39 +08:00
|
|
|
|
Column({ space: 0 }) {
|
|
|
|
|
|
if (this.memories.length === 0) {
|
|
|
|
|
|
// 空态与网页版一字不差(两句:一句说没有,一句给个盼头)
|
|
|
|
|
|
Column({ space: 6 }) {
|
|
|
|
|
|
Text('往年的这一天还没有记录')
|
2026-10-11 22:07:48 +08:00
|
|
|
|
.fontSize(FbMetric.fontSmall)
|
|
|
|
|
|
.fontColor(FbColors.ink)
|
2026-10-18 03:35:39 +08:00
|
|
|
|
Text('日子还长,等有了第一次,明年这时候就会自己冒出来。')
|
2026-10-11 22:07:48 +08:00
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(FbColors.muted)
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
2026-10-18 03:35:39 +08:00
|
|
|
|
.alignItems(HorizontalAlign.Start)
|
|
|
|
|
|
.padding({ top: 12, bottom: 12 })
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ForEach(this.memories, (m: FbMemoryItem, idx: number) => {
|
|
|
|
|
|
this.memoryRow(m, idx === this.memories.length - 1)
|
|
|
|
|
|
}, (m: FbMemoryItem, idx: number): string => 'mem_' + m.kind + '_' + m.id.toString())
|
|
|
|
|
|
}
|
2026-10-11 22:07:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
2026-10-18 03:35:39 +08:00
|
|
|
|
.padding({ left: FbMetric.cardPad, right: FbMetric.cardPad, top: 4, bottom: 6 })
|
2026-10-11 22:07:48 +08:00
|
|
|
|
.backgroundColor(FbColors.surface)
|
|
|
|
|
|
.borderRadius(FbMetric.radius)
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12 })
|
|
|
|
|
|
|
2026-10-18 03:35:39 +08:00
|
|
|
|
// 🔴🔴 **底部悬浮快捷栏的预留** —— 它必须留在**页面最根部**。
|
|
|
|
|
|
// v54 之前它被接在了概述卡中间那个小 Flex 上(96vp 凭空撑高卡片),
|
|
|
|
|
|
// 真正的页面底部反而没有留白、被悬浮栏压住最后一屏。
|
|
|
|
|
|
// ⚠️ 别再把它挪到任何中间区块上。
|
|
|
|
|
|
Blank().height(FB_DOCK_RESERVE)
|
2026-10-11 22:07:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
2026-10-21 20:01:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.height('100%')
|
|
|
|
|
|
.scrollBar(BarState.Off)
|
|
|
|
|
|
.edgeEffect(EdgeEffect.Spring)
|
|
|
|
|
|
// 🔴 滚动方向上报(驱动底部悬浮栏随滚收起 / 唤回)—— 见 common/FbScroll.ets
|
|
|
|
|
|
.onDidScroll(fbOnScroll)
|
|
|
|
|
|
|
|
|
|
|
|
// 🔴 触摸方向兜底(内容不足一屏的页面滚不动,只能靠手指位移判断)
|
|
|
|
|
|
// 与上面的 `onDidScroll` 互补、互不冲突 —— 见 common/FbScroll.ets
|
|
|
|
|
|
.onTouch(fbOnTouch)
|
|
|
|
|
|
.align(Alignment.TopStart)
|
|
|
|
|
|
|
2026-10-24 19:14:18 +08:00
|
|
|
|
// ==================== 新建日程(v84)====================
|
|
|
|
|
|
// ⚠️ 遮罩与表单是 Stack 的**兄弟**,不是 Column 的子项
|
|
|
|
|
|
// (本项目踩过:`Column { 整高遮罩; 卡片 }` 会把卡片挤出屏幕,
|
|
|
|
|
|
// 编译通过、节点树里却一个节点都没有 —— 看起来像"点了没反应")。
|
|
|
|
|
|
if (this.calCreating) {
|
|
|
|
|
|
Column()
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.height('100%')
|
|
|
|
|
|
.backgroundColor(FB_MASK)
|
|
|
|
|
|
.transition(fbMaskTransition())
|
|
|
|
|
|
.onClick((): void => {
|
|
|
|
|
|
this.calCreating = false;
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
FbEventEditSheet({
|
|
|
|
|
|
event: null,
|
|
|
|
|
|
isCreate: true,
|
|
|
|
|
|
/* 首页这条区块叫「今天的日程」⇒ 默认给今天 */
|
|
|
|
|
|
presetDate: this.todayIso(),
|
|
|
|
|
|
books: this.calBooks,
|
|
|
|
|
|
onCancel: (): void => {
|
|
|
|
|
|
this.calCreating = false;
|
|
|
|
|
|
},
|
|
|
|
|
|
onSaved: (): void => {
|
|
|
|
|
|
this.calCreating = false;
|
|
|
|
|
|
/* 存成功立刻重拉首页 ⇒ 新日程出现在「今天的日程」里。
|
|
|
|
|
|
⚠️ 必须重拉而不是"本地塞一条":服务端会补 id / 颜色 / 日历本名,
|
|
|
|
|
|
本地塞的那条会和之后下拉刷新出来的长得不一样。 */
|
|
|
|
|
|
this.load();
|
|
|
|
|
|
},
|
|
|
|
|
|
onDeleted: (): void => {
|
|
|
|
|
|
this.calCreating = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.transition(fbSheetTransition())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-21 20:01:37 +08:00
|
|
|
|
// ==================== 日程详情卡(v73)====================
|
|
|
|
|
|
// ⚠️ 遮罩与卡片是 Stack 的**兄弟**,不是 Column 的子项(见 build 开头那段说明)。
|
|
|
|
|
|
if (this.evtBase !== null) {
|
|
|
|
|
|
Column()
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.height('100%')
|
|
|
|
|
|
.backgroundColor(FB_MASK)
|
|
|
|
|
|
.transition(fbMaskTransition())
|
|
|
|
|
|
.onClick((): void => {
|
|
|
|
|
|
this.closeEvent();
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
FbEventSheet({
|
|
|
|
|
|
event: this.evtFull,
|
|
|
|
|
|
loading: this.evtLoading,
|
|
|
|
|
|
error: this.evtError,
|
|
|
|
|
|
accent: this.calAccent(),
|
|
|
|
|
|
bookName: this.evtBase !== null ? this.evtBase.calName : '',
|
|
|
|
|
|
bookColor: this.evtBase !== null ? this.evtBase.color : '#9AA0A6',
|
2026-10-24 20:22:35 +08:00
|
|
|
|
/* 🔴 v86(用户 2026-10-24「端上也全部都实现」):**打开编辑入口**。
|
|
|
|
|
|
老注释说这里是"有意 false",理由是编辑要跳模块、"用户会看到卡闪一下没了"。
|
|
|
|
|
|
那个理由成立,但**结论错了** —— 正确解法不是"不给编辑",
|
|
|
|
|
|
而是"编辑也不许跳"(与 v79 任务面板的处理完全一致)。
|
|
|
|
|
|
现在点「编辑」只在本页叠一张表单,卡片不动、模块不动。 */
|
2026-10-29 20:39:52 +08:00
|
|
|
|
/* 🔴 2026-10-29 真机实测修(工单 laoyang/FamilyBook#31):
|
|
|
|
|
|
原来这里写死 `true`,于是**只读共享本**(如「工作学习」)里的日程
|
|
|
|
|
|
也显示「编辑日程」,用户改完点保存才被服务端拒
|
|
|
|
|
|
「保存失败:这本日历你没有写权限。」
|
|
|
|
|
|
v86 开编辑入口的决定本身没错(编辑就地改、不跳页),
|
|
|
|
|
|
错在没把"没有写权限的本"排除掉 ⇒ 改为按 calWrite 判定。 */
|
|
|
|
|
|
canEdit: this.evtBase !== null && isEventWritable(this.evtBase, this.calBooks),
|
2026-11-05 16:21:16 +08:00
|
|
|
|
onNavigate: (navTarget: string): void => {
|
|
|
|
|
|
this.navigateEvent(navTarget);
|
2026-10-21 20:01:37 +08:00
|
|
|
|
},
|
|
|
|
|
|
onClose: (): void => {
|
|
|
|
|
|
this.closeEvent();
|
|
|
|
|
|
},
|
|
|
|
|
|
onEdit: (): void => {
|
|
|
|
|
|
this.editEventFromDetail();
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.transition(fbSheetTransition())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-24 20:22:35 +08:00
|
|
|
|
// ==================== 编辑日程(v86:就地改,不跳日程页)====================
|
|
|
|
|
|
/* 🔴🔴 **必须放在「日程详情卡」之后**(Stack 里后面的盖在前面的上面)。
|
|
|
|
|
|
⚠️⚠️ 2026-10-24 真机事故(本轮):我上一版把这一块**写在了详情卡前面**,
|
|
|
|
|
|
注释里写着"必须放在详情卡之后",代码却反着放 —— 于是详情卡的整屏遮罩
|
|
|
|
|
|
盖在编辑表单**上面**,表单看得见、**所有按钮都点不动**
|
|
|
|
|
|
(实测连右上角「取消」都点不动,`dumpLayout` 里坐标一切正常)。
|
|
|
|
|
|
⇒ 这就是"看着做完了、其实用不了",而**所有"节点存在"类断言全绿**。
|
|
|
|
|
|
⚠️ 只靠注释约束顺序是不够的,守卫里已经有专门一条守它(见 groupV86)。 */
|
|
|
|
|
|
if (this.calEditing && this.evtFull !== null) {
|
|
|
|
|
|
Column()
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.height('100%')
|
|
|
|
|
|
.backgroundColor(FB_MASK)
|
|
|
|
|
|
.transition(fbMaskTransition())
|
|
|
|
|
|
.onClick((): void => {
|
|
|
|
|
|
this.calEditing = false;
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
FbEventEditSheet({
|
|
|
|
|
|
event: this.evtFull,
|
|
|
|
|
|
isCreate: false,
|
|
|
|
|
|
books: this.calBooks,
|
|
|
|
|
|
onCancel: (): void => {
|
|
|
|
|
|
this.calEditing = false;
|
|
|
|
|
|
},
|
|
|
|
|
|
onSaved: (): void => {
|
|
|
|
|
|
this.calEditing = false;
|
|
|
|
|
|
/* 重拉首页列表(摘要卡、条数、统计都会变) */
|
|
|
|
|
|
this.load();
|
|
|
|
|
|
/* 🔴 详情卡还开着 ⇒ 它显示的是 `evtFull` 那份**旧**数据,
|
|
|
|
|
|
不重拉就会出现"表单里改了标题、关掉表单后卡片还是旧标题"。
|
|
|
|
|
|
复用 `openEvent` 那套拉取逻辑(含"先给壳再给全量"的降级)。
|
|
|
|
|
|
⚠️ 传 `evtBase` 而不是 `evtFull`:`openEvent` 的入参是列表行类型。 */
|
|
|
|
|
|
if (this.evtBase !== null) {
|
|
|
|
|
|
this.openEvent(this.evtBase);
|
|
|
|
|
|
}
|
|
|
|
|
|
this.pushCards();
|
|
|
|
|
|
},
|
|
|
|
|
|
onDeleted: (): void => {
|
|
|
|
|
|
/* 在编辑表单里删掉了这条 ⇒ 详情卡不能留在屏幕上(它指向已不存在的记录) */
|
|
|
|
|
|
this.calEditing = false;
|
|
|
|
|
|
this.closeEvent();
|
|
|
|
|
|
this.load();
|
|
|
|
|
|
this.pushCards();
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.transition(fbSheetTransition())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-22 16:40:44 +08:00
|
|
|
|
// ==================== 任务面板(v79:可就地处理)====================
|
|
|
|
|
|
/* 🔴 用户 2026-10-22:「在首页上可以直接修改编辑任务。」
|
|
|
|
|
|
⇒ v73 的"只读详情卡"升级成面板:状态 / 看板 / 子任务 / 附件 / 留言
|
|
|
|
|
|
全在这张卡里做完,**不跳任务模块**(跳模块是用户明确否掉的)。
|
|
|
|
|
|
⚠️ 面板根节点自带遮罩与 `Stack`,这里**不要**再包一层遮罩 ——
|
|
|
|
|
|
包了就会有两层暗色叠在一起(越点越黑)。 */
|
2026-10-21 20:01:37 +08:00
|
|
|
|
if (this.tskBase !== null) {
|
2026-10-22 16:40:44 +08:00
|
|
|
|
FbTaskPanel({
|
|
|
|
|
|
taskId: this.tskBase.id,
|
|
|
|
|
|
seed: this.tskBase,
|
|
|
|
|
|
boards: this.tskBoards,
|
2026-11-05 20:15:25 +08:00
|
|
|
|
cats: this.tskCats,
|
2026-10-21 20:01:37 +08:00
|
|
|
|
accent: this.taskAccent(),
|
2026-10-22 16:40:44 +08:00
|
|
|
|
refreshTick: this.tskTick,
|
2026-10-21 20:01:37 +08:00
|
|
|
|
onClose: (): void => {
|
|
|
|
|
|
this.closeTask();
|
2026-10-22 16:40:44 +08:00
|
|
|
|
},
|
|
|
|
|
|
onEdit: (d: FbTaskDetail): void => {
|
|
|
|
|
|
this.openTaskEdit(d);
|
|
|
|
|
|
},
|
|
|
|
|
|
onChanged: (): void => {
|
|
|
|
|
|
/* 一次动作会牵动"近期任务"这张卡的排序/计数(服务端算的)⇒ 重拉首页 */
|
|
|
|
|
|
this.load();
|
|
|
|
|
|
this.pushCards();
|
|
|
|
|
|
},
|
|
|
|
|
|
onDeleted: (): void => {
|
|
|
|
|
|
this.closeTask();
|
|
|
|
|
|
this.load();
|
|
|
|
|
|
this.pushCards();
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.transition(fbSheetTransition())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 任务编辑表单(v79:就地改,不跳任务页)====================
|
|
|
|
|
|
if (this.tskEditOpen) {
|
|
|
|
|
|
FbTaskEditSheet({
|
|
|
|
|
|
accent: this.taskAccent(),
|
|
|
|
|
|
boards: this.tskBoards,
|
|
|
|
|
|
cats: this.tskCats,
|
|
|
|
|
|
editingId: this.tskEditId,
|
|
|
|
|
|
editingTitle: this.tskEditTitle,
|
|
|
|
|
|
editingNote: this.tskEditNote,
|
|
|
|
|
|
editingDueAt: this.tskEditDueAt,
|
|
|
|
|
|
editingBoardId: this.tskEditBoardId,
|
|
|
|
|
|
editingCatId: this.tskEditCatId,
|
|
|
|
|
|
editingRepeat: this.tskEditRepeat,
|
2026-11-05 12:41:04 +08:00
|
|
|
|
editingStatus: this.tskEditStatus,
|
2026-10-22 16:40:44 +08:00
|
|
|
|
onClose: (): void => {
|
|
|
|
|
|
this.tskEditOpen = false;
|
|
|
|
|
|
},
|
|
|
|
|
|
onSaved: (): void => {
|
|
|
|
|
|
this.tskEditOpen = false;
|
|
|
|
|
|
/* 面板还开着 ⇒ 让它自己重拉一次(标题/截止/分类都变了) */
|
|
|
|
|
|
this.tskTick = this.tskTick + 1;
|
|
|
|
|
|
this.load();
|
|
|
|
|
|
this.pushCards();
|
2026-10-21 20:01:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.transition(fbSheetTransition())
|
|
|
|
|
|
}
|
2026-10-24 20:22:35 +08:00
|
|
|
|
|
|
|
|
|
|
// ==================== 新建纪念日(v86)====================
|
|
|
|
|
|
/* ⚠️ 三个新表单各自一个**独立布尔**,故意不复用:
|
|
|
|
|
|
复用会让"打开哪一个"取决于一堆隐式状态,`backOnce` 也没法按层关。
|
|
|
|
|
|
⚠️ 遮罩都必须是 Stack 的**独立兄弟**(见 build 开头那段事故说明)。 */
|
|
|
|
|
|
if (this.dayCreating) {
|
|
|
|
|
|
Column()
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.height('100%')
|
|
|
|
|
|
.backgroundColor(FB_MASK)
|
|
|
|
|
|
.transition(fbMaskTransition())
|
|
|
|
|
|
.onClick((): void => {
|
|
|
|
|
|
this.dayCreating = false;
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
FbDayEditSheet({
|
|
|
|
|
|
accent: this.daysAccent(),
|
|
|
|
|
|
/* `editingId: 0` = 新建(与纪念日页 `openNew()` 同一个约定)。
|
|
|
|
|
|
⚠️ 其余字段虽然新建用不到,但**必须显式给默认值**:
|
|
|
|
|
|
组件是 `@Param`,不传就沿用上一次的残留(页面常驻不重建)。 */
|
|
|
|
|
|
editingId: 0,
|
|
|
|
|
|
editingTitle: '',
|
|
|
|
|
|
editingKind: '',
|
|
|
|
|
|
editingNote: '',
|
|
|
|
|
|
editingRepeat: 'solar',
|
|
|
|
|
|
editingMonth: new Date().getMonth() + 1,
|
|
|
|
|
|
editingDay: new Date().getDate(),
|
|
|
|
|
|
editingPersonIds: [],
|
|
|
|
|
|
roster: this.dayRoster,
|
|
|
|
|
|
onClose: (): void => {
|
|
|
|
|
|
this.dayCreating = false;
|
|
|
|
|
|
},
|
|
|
|
|
|
onSaved: (): void => {
|
|
|
|
|
|
this.dayCreating = false;
|
|
|
|
|
|
/* 「最近纪念日」是**服务端算的**(`recent_days`)⇒ 必须重拉,
|
|
|
|
|
|
本地塞一条排不出"最近三个"的正确顺序。 */
|
|
|
|
|
|
this.load();
|
|
|
|
|
|
},
|
|
|
|
|
|
onDeleted: (): void => {
|
|
|
|
|
|
this.dayCreating = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.transition(fbSheetTransition())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-05 13:43:03 +08:00
|
|
|
|
// ==================== #63:最近观影详情 ====================
|
|
|
|
|
|
/* 🔴 用户:「"最近观影"点了之后没反应…点了之后应该有反应,
|
|
|
|
|
|
要么弹出来详细信息。」
|
|
|
|
|
|
⚠️ 复用电影页的同一张卡(`FbMovieDetailSheet`),
|
|
|
|
|
|
不为首页另写一套 —— 否则改一处要改两处。 */
|
|
|
|
|
|
if (this.mvDetail !== null || this.mvDetailLoading
|
|
|
|
|
|
|| this.mvDetailError !== '') {
|
|
|
|
|
|
FbMovieDetailSheet({
|
|
|
|
|
|
detail: this.mvDetail,
|
|
|
|
|
|
loading: this.mvDetailLoading,
|
|
|
|
|
|
error: this.mvDetailError,
|
|
|
|
|
|
listRating: this.mvSeed !== null ? this.mvSeed.reviewAvg : 0,
|
|
|
|
|
|
listReviewCount: this.mvSeed !== null ? this.mvSeed.reviewCount : 0,
|
|
|
|
|
|
accent: this.moviesAccent(),
|
|
|
|
|
|
onClose: (): void => {
|
|
|
|
|
|
this.closeMovieDetail();
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.transition(fbSheetTransition())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== #64:联系人详情(可打电话)====================
|
|
|
|
|
|
/* 🔴 用户:「点击之后弹出来详细信息,就跟名册里面那个人的列表
|
|
|
|
|
|
点击完之后的效果是一样的,这样可以直接打电话。」
|
|
|
|
|
|
⚠️ 复用名册那张卡(`FbPersonSheet`),它自带手机号拨号入口。 */
|
|
|
|
|
|
if (this.pfDetail !== null) {
|
|
|
|
|
|
FbPersonSheet({
|
|
|
|
|
|
person: this.pfDetail,
|
|
|
|
|
|
accent: this.peopleAccent(),
|
|
|
|
|
|
canDelete: false, // 首页进来的不给删(避免误删),要删去名册
|
|
|
|
|
|
onClose: (): void => {
|
|
|
|
|
|
this.closePersonDetail();
|
|
|
|
|
|
},
|
|
|
|
|
|
onEdit: (): void => {
|
|
|
|
|
|
/* 关掉详情卡,跳到名册去改 —— 首页没有人员编辑表单,
|
|
|
|
|
|
与其在这里硬塞一份,不如交给名册(那是它的主场)。 */
|
|
|
|
|
|
this.closePersonDetail();
|
|
|
|
|
|
this.app.switchModule('people');
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.transition(fbSheetTransition())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== #62/#63:纪念日详情 ====================
|
|
|
|
|
|
/* 🔴 用户:「近期任务、纪念日、最近观影都像日程一样,弹出来一个卡片,
|
|
|
|
|
|
点编辑的时候才进入编辑。」
|
|
|
|
|
|
⚠️ 复用 `FbDayEditSheet`(它就是"回填一条已有纪念日"的表单)。
|
|
|
|
|
|
它的保存/删除按钮在卡里,看着是"可改的详情卡",
|
|
|
|
|
|
符合用户"先看到信息"的要求。 */
|
|
|
|
|
|
if (this.dayDetail !== null) {
|
|
|
|
|
|
FbDayEditSheet({
|
|
|
|
|
|
accent: this.daysAccent(),
|
|
|
|
|
|
editingId: this.dayDetail.id,
|
|
|
|
|
|
editingTitle: this.dayDetail.title,
|
|
|
|
|
|
editingKind: this.dayDetail.kind,
|
|
|
|
|
|
editingNote: '',
|
|
|
|
|
|
editingRepeat: 'solar',
|
|
|
|
|
|
editingMonth: 1,
|
|
|
|
|
|
editingDay: 1,
|
|
|
|
|
|
editingPersonIds: this.dayDetail.personId > 0 ? [this.dayDetail.personId] : [],
|
|
|
|
|
|
roster: this.dayRoster,
|
|
|
|
|
|
onClose: (): void => {
|
|
|
|
|
|
this.closeDayDetail();
|
|
|
|
|
|
},
|
|
|
|
|
|
onSaved: (): void => {
|
|
|
|
|
|
this.closeDayDetail();
|
|
|
|
|
|
this.load();
|
|
|
|
|
|
},
|
|
|
|
|
|
onDeleted: (): void => {
|
|
|
|
|
|
this.closeDayDetail();
|
|
|
|
|
|
this.load();
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.transition(fbSheetTransition())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== #65:往年今天的通用摘要卡 ====================
|
|
|
|
|
|
/* 电影类已走 `openMovieDetail`(弹电影卡);
|
|
|
|
|
|
这里只剩 `trip_node` 等拿不到 tripId 的来源 ⇒ 给只读摘要。
|
|
|
|
|
|
⚠️ 弹错类型的卡比不弹更糟,所以宁可保守。 */
|
|
|
|
|
|
if (this.memDetail !== null) {
|
|
|
|
|
|
this.memorySheet(this.memDetail)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-24 20:22:35 +08:00
|
|
|
|
// ==================== 新增电影 / 记一场(v86)====================
|
|
|
|
|
|
if (this.mvCreating) {
|
|
|
|
|
|
Column()
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.height('100%')
|
|
|
|
|
|
.backgroundColor(FB_MASK)
|
|
|
|
|
|
.transition(fbMaskTransition())
|
|
|
|
|
|
.onClick((): void => {
|
|
|
|
|
|
this.mvCreating = false;
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
FbMovieNewSheet({
|
|
|
|
|
|
cinemas: this.mvCinemas,
|
|
|
|
|
|
accent: this.moviesAccent(),
|
|
|
|
|
|
/* 「同看的人」默认带上**我**(与电影页同一个口径:自己不用选就该已选上)。
|
|
|
|
|
|
⚠️ 取不到(0)时组件内部不会硬塞一个错的 id。 */
|
|
|
|
|
|
meId: this.app.session.me.id,
|
|
|
|
|
|
onClose: (): void => {
|
|
|
|
|
|
this.mvCreating = false;
|
|
|
|
|
|
},
|
|
|
|
|
|
onCreated: (): void => {
|
|
|
|
|
|
this.mvCreating = false;
|
|
|
|
|
|
/* 本地库已经在 `createMovie()` 里重拉过了 ⇒ 这里只重读卡片那几条 */
|
|
|
|
|
|
this.reloadSideCards();
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.transition(fbSheetTransition())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 新增成员(v86)====================
|
|
|
|
|
|
if (this.pfCreating) {
|
|
|
|
|
|
Column()
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.height('100%')
|
|
|
|
|
|
.backgroundColor(FB_MASK)
|
|
|
|
|
|
.transition(fbMaskTransition())
|
|
|
|
|
|
.onClick((): void => {
|
|
|
|
|
|
this.pfCreating = false;
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
FbPersonEditSheet({
|
|
|
|
|
|
person: null,
|
|
|
|
|
|
isCreate: true,
|
|
|
|
|
|
/* 已有标签供点选(不传的话用户只能手打,很容易出现"同事"和"同 事"两个标签) */
|
|
|
|
|
|
knownTags: this.personKnownTags(),
|
|
|
|
|
|
accent: this.peopleAccent(),
|
|
|
|
|
|
onCancel: (): void => {
|
|
|
|
|
|
this.pfCreating = false;
|
|
|
|
|
|
},
|
|
|
|
|
|
onSaved: (): void => {
|
|
|
|
|
|
this.pfCreating = false;
|
|
|
|
|
|
this.reloadSideCards();
|
|
|
|
|
|
},
|
|
|
|
|
|
onDeleted: (): void => {
|
|
|
|
|
|
/* 新建模式没有删除,走不到这里;置 false 只是兜底 */
|
|
|
|
|
|
this.pfCreating = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.transition(fbSheetTransition())
|
|
|
|
|
|
}
|
2026-10-11 22:07:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.height('100%')
|
2026-10-21 20:01:37 +08:00
|
|
|
|
.alignContent(Alignment.TopStart)
|
2026-10-11 22:07:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-18 03:35:39 +08:00
|
|
|
|
/** 天气地点行:`青岛市 · 崂山区 · 东南风 12 km/h` */
|
|
|
|
|
|
private weatherLine(): string {
|
|
|
|
|
|
const w: FbWeather | null = this.weather;
|
|
|
|
|
|
if (w === null) {
|
|
|
|
|
|
return '';
|
|
|
|
|
|
}
|
|
|
|
|
|
const parts: string[] = [];
|
|
|
|
|
|
if (w.place !== '') {
|
|
|
|
|
|
parts.push(w.place);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (w.tmax !== '' || w.tmin !== '') {
|
|
|
|
|
|
parts.push(w.tmax + ' / ' + w.tmin);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (w.wind !== '') {
|
|
|
|
|
|
parts.push(w.wdir !== '' ? (w.wdir + ' ' + w.wind) : w.wind);
|
|
|
|
|
|
}
|
|
|
|
|
|
return parts.join(' · ');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 「近期任务」右上角小字:`3 件在做 · 5 件待办`(进行中与待办**分开数**) */
|
|
|
|
|
|
private taskExtra(): string {
|
|
|
|
|
|
let doing: number = 0;
|
|
|
|
|
|
let todo: number = 0;
|
|
|
|
|
|
for (const t of this.tasks) {
|
|
|
|
|
|
if (t.status === 'doing') {
|
|
|
|
|
|
doing++;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
todo++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (todo > 0) {
|
2026-11-05 12:41:04 +08:00
|
|
|
|
return doing.toString() + ' 件在做 · ' + todo.toString() + ' 件待做';
|
2026-10-18 03:35:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
return doing.toString() + ' 件在做';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 一条任务(首页版)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 🔴 与网页版首页同一套信息:圆点(看板色,逾期变红)+ 标题 + **状态词**
|
|
|
|
|
|
* (用户第 3 条:「除了显示任务的标题之外,还要显示他的状态」)
|
|
|
|
|
|
* + 子任务进度 + 截止时间。
|
|
|
|
|
|
* ⚠️ 看板色**必须**用真实派生色(`boardColor`)—— 用户 2026-10-08 报过
|
|
|
|
|
|
* 「首页近期任务……颜色并没有跟看板颜色一致」。
|
2026-10-21 20:01:37 +08:00
|
|
|
|
*
|
|
|
|
|
|
* 🔴 v73(用户 2026-10-21):「任务也是一样,点击任务之后弹出来详情,
|
|
|
|
|
|
* 可以标记它是不是完成了什么之类的。」
|
|
|
|
|
|
* ⇒ 整行可点,弹 `FbTaskDetailSheet`(**不再跳任务模块**)。
|
|
|
|
|
|
* ⚠️ 这是 `@Builder taskRow(t, isLast)` —— **两个参数**,本项目的著名反模式。
|
|
|
|
|
|
* 它在这里是安全的,理由与 `memoryRow` 完全相同:
|
|
|
|
|
|
* 列表由 `ForEach` 按 `t.id` 重建(数据变化 ⇒ 增删列表项 ⇒ 走重建路径,
|
|
|
|
|
|
* 不依赖 Builder 参数刷新)。⚠️ 如果哪天出现"数据变了、这一行文字没变",
|
|
|
|
|
|
* **先改成组件**(参照 `FbStatCell`),别在这个 Builder 上打补丁。
|
2026-10-18 03:35:39 +08:00
|
|
|
|
*/
|
2026-10-11 22:07:48 +08:00
|
|
|
|
@Builder
|
2026-10-18 03:35:39 +08:00
|
|
|
|
taskRow(t: FbTaskItem, isLast: boolean) {
|
|
|
|
|
|
Column({ space: 0 }) {
|
|
|
|
|
|
Row({ space: 10 }) {
|
|
|
|
|
|
// 圆点:逾期用红(状态语义优先),否则跟看板色
|
|
|
|
|
|
Row()
|
|
|
|
|
|
.width(8)
|
|
|
|
|
|
.height(8)
|
|
|
|
|
|
.borderRadius(4)
|
|
|
|
|
|
.backgroundColor(t.overdue ? '#C0533F' : t.boardColor)
|
|
|
|
|
|
|
|
|
|
|
|
Text(t.title)
|
|
|
|
|
|
.fontSize(FbMetric.fontSmall)
|
|
|
|
|
|
.fontColor(t.overdue ? '#C0533F' : FbColors.ink)
|
|
|
|
|
|
.layoutWeight(1)
|
|
|
|
|
|
.maxLines(1)
|
|
|
|
|
|
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
|
|
|
|
|
|
|
|
|
|
|
// 状态词(待办 / 进行中)—— 用户第 3 条要求
|
2026-11-05 12:41:04 +08:00
|
|
|
|
Text(t.status === 'doing' ? '进行中' : '待做')
|
2026-10-18 03:35:39 +08:00
|
|
|
|
.fontSize(10)
|
|
|
|
|
|
.fontColor(t.status === 'doing' ? '#A8574F' : FbColors.faint)
|
|
|
|
|
|
.backgroundColor(t.status === 'doing' ? '#14A8574F' : FbColors.inputBg)
|
|
|
|
|
|
.borderRadius(FbMetric.radiusPill)
|
|
|
|
|
|
.padding({ left: 6, right: 6, top: 1, bottom: 1 })
|
|
|
|
|
|
|
|
|
|
|
|
if (t.childTotal > 0) {
|
|
|
|
|
|
Text(t.childDone.toString() + '/' + t.childTotal.toString())
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(FbColors.faint)
|
|
|
|
|
|
}
|
|
|
|
|
|
if (t.dueText.length > 0) {
|
|
|
|
|
|
Text(t.dueText)
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(t.overdue ? '#C0533F' : FbColors.faint)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding({ top: 11, bottom: 11 })
|
|
|
|
|
|
.alignItems(VerticalAlign.Center)
|
2026-10-21 20:01:37 +08:00
|
|
|
|
/* 🔴 v73:整行点开任务详情卡。
|
|
|
|
|
|
⚠️ 热区放在这个 `Row` 上(它含圆点/标题/状态/截止,一行都算),
|
|
|
|
|
|
不是放在 `Column` 上 —— Column 还包着分隔线,点分隔线也会触发,
|
|
|
|
|
|
两者视觉上不是"同一件东西"。
|
|
|
|
|
|
⚠️ 手指反馈:`stateStyles` 用不到(这里不是 Button),
|
|
|
|
|
|
所以靠"点开后有一张卡弹出来"作为反馈 —— 那已经足够强。 */
|
|
|
|
|
|
.onClick((): void => {
|
|
|
|
|
|
this.openTask(t);
|
|
|
|
|
|
})
|
2026-10-18 03:35:39 +08:00
|
|
|
|
|
|
|
|
|
|
if (!isLast) {
|
|
|
|
|
|
Divider()
|
|
|
|
|
|
.color(FbColors.line2)
|
|
|
|
|
|
.margin({ left: 18 })
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 一条「往年的今天」。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 形态照网页版 `templates/home.php` 的 `.dash-row`:
|
|
|
|
|
|
* 圆点(旅程=绿 #3F7D5A / 观影=紫 #7A4E8C)→「N 年前」→ 🧳/🎬 + 标题 → 日期。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 这里是 `@Builder` 带 **2 个参数** —— 与本次概述卡数字那个 bug 同一个反模式。
|
|
|
|
|
|
* 但两者的**后果不同**:本区块的数据(`this.memories`)是在页面首次加载后
|
|
|
|
|
|
* 随其它字段一起到的,且整块由 `ForEach` 按 `m.id` 重建
|
|
|
|
|
|
* (数据变化 ⇒ 列表项增删 ⇒ 走 ForEach 的重建路径,不依赖 Builder 参数刷新)。
|
|
|
|
|
|
* ⚠️ 若将来出现"memories 变了、这一行文字没变"的现象,**先改成组件**
|
|
|
|
|
|
* (参照 `FbStatCell`),不要在这个 Builder 上打补丁。
|
|
|
|
|
|
*/
|
|
|
|
|
|
@Builder
|
|
|
|
|
|
memoryRow(m: FbMemoryItem, isLast: boolean) {
|
|
|
|
|
|
Column({ space: 0 }) {
|
|
|
|
|
|
Row({ space: 10 }) {
|
|
|
|
|
|
// 圆点:两类的颜色与网页版一致(旅程绿 / 观影紫)
|
|
|
|
|
|
Row()
|
|
|
|
|
|
.width(8)
|
|
|
|
|
|
.height(8)
|
|
|
|
|
|
.borderRadius(4)
|
|
|
|
|
|
.backgroundColor(m.kind === 'trip_node' ? '#3F7D5A' : '#7A4E8C')
|
|
|
|
|
|
|
|
|
|
|
|
// 「N 年前」
|
|
|
|
|
|
Text(m.yearsAgo.toString() + ' 年前')
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(FbColors.muted)
|
|
|
|
|
|
.width(52)
|
|
|
|
|
|
|
|
|
|
|
|
// 🧳 / 🎬 + 标题
|
|
|
|
|
|
Text((m.kind === 'trip_node' ? '🧳 ' : '🎬 ') + m.title)
|
2026-10-11 22:07:48 +08:00
|
|
|
|
.fontSize(FbMetric.fontSmall)
|
|
|
|
|
|
.fontColor(FbColors.ink)
|
|
|
|
|
|
.layoutWeight(1)
|
|
|
|
|
|
.maxLines(1)
|
2026-10-18 03:35:39 +08:00
|
|
|
|
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
|
|
|
|
|
|
|
|
|
|
|
// 那天的日期(`YYYY-MM-DD`)
|
|
|
|
|
|
Text(this.memoryDate(m.startsAt))
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(FbColors.faint)
|
2026-10-11 22:07:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
2026-10-18 03:35:39 +08:00
|
|
|
|
.padding({ top: 11, bottom: 11 })
|
2026-10-11 22:07:48 +08:00
|
|
|
|
.alignItems(VerticalAlign.Center)
|
|
|
|
|
|
|
2026-10-18 03:35:39 +08:00
|
|
|
|
if (!isLast) {
|
|
|
|
|
|
Divider()
|
|
|
|
|
|
.color(FbColors.line2)
|
|
|
|
|
|
.margin({ left: 18 })
|
|
|
|
|
|
}
|
2026-10-11 22:07:48 +08:00
|
|
|
|
}
|
2026-10-18 03:35:39 +08:00
|
|
|
|
.width('100%')
|
2026-11-05 13:43:03 +08:00
|
|
|
|
.onClick((): void => {
|
|
|
|
|
|
this.openMemoryDetail(m);
|
|
|
|
|
|
})
|
2026-10-11 22:07:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-18 03:35:39 +08:00
|
|
|
|
/** `YYYY-MM-DD HH:MM:SS` → `YYYY-MM-DD`(只取日期,与网页版 `substr(...,0,10)` 一致) */
|
|
|
|
|
|
private memoryDate(s: string): string {
|
|
|
|
|
|
return s.length >= 10 ? s.substring(0, 10) : s;
|
2026-10-11 22:07:48 +08:00
|
|
|
|
}
|
2026-10-18 20:14:40 +08:00
|
|
|
|
|
2026-10-24 20:22:35 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 「最近观影」里的一行(v86)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 形态照网页端 `_card_movies.php`:**片名**(主)+ 日期 · 影院(次)。
|
|
|
|
|
|
* ⚠️ 不放海报:卡片上放图要么挤成小方块看不清、要么整行高度被拉起来,
|
|
|
|
|
|
* 而且每行拉一张图 = 首页多几个请求。看海报请进电影页。
|
|
|
|
|
|
* ⚠️ 又是**两参数 @Builder**(`isLast` 只为画分隔线)——
|
|
|
|
|
|
* 与 `memoryRow` / `recentDayRow` 同一个安全前提:整块由 ForEach 按 key 重建。
|
|
|
|
|
|
*/
|
|
|
|
|
|
@Builder
|
|
|
|
|
|
movieRow(m: FbMovieListItem, isLast: boolean) {
|
|
|
|
|
|
Column({ space: 0 }) {
|
|
|
|
|
|
Row({ space: 10 }) {
|
|
|
|
|
|
/* 小圆角色块代替海报:用影院色(`cinemaColor`)
|
|
|
|
|
|
—— 既区分了影院、又不吃网络请求。
|
|
|
|
|
|
⚠️ 影院色可能是空串(没设影院的老数据)⇒ 兜底一个中性灰。 */
|
|
|
|
|
|
Column() {
|
|
|
|
|
|
Text('🎬')
|
|
|
|
|
|
.fontSize(15)
|
|
|
|
|
|
}
|
|
|
|
|
|
.width(34)
|
|
|
|
|
|
.height(34)
|
|
|
|
|
|
.borderRadius(9)
|
|
|
|
|
|
.backgroundColor(this.movieTint(m.cinemaColor))
|
|
|
|
|
|
.justifyContent(FlexAlign.Center)
|
|
|
|
|
|
.alignItems(HorizontalAlign.Center)
|
|
|
|
|
|
|
|
|
|
|
|
Column({ space: 3 }) {
|
|
|
|
|
|
Text(m.title !== '' ? m.title : '(未命名)')
|
|
|
|
|
|
.fontSize(FbMetric.fontBody)
|
|
|
|
|
|
.fontColor(FbColors.ink)
|
|
|
|
|
|
.maxLines(1)
|
|
|
|
|
|
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.textAlign(TextAlign.Start)
|
|
|
|
|
|
|
|
|
|
|
|
/* 日期 + 影院。影院名可能很长("万达影城(XX 广场店)")⇒ 一起截断,
|
|
|
|
|
|
用 `maxLines(1)` 保一行 —— 卡片上不该因为影院名换行而高一行。 */
|
|
|
|
|
|
Text(this.movieSub(m))
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(FbColors.muted)
|
|
|
|
|
|
.maxLines(1)
|
|
|
|
|
|
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.textAlign(TextAlign.Start)
|
|
|
|
|
|
}
|
|
|
|
|
|
.layoutWeight(1)
|
|
|
|
|
|
.alignItems(HorizontalAlign.Start)
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding({ top: 10, bottom: 10 })
|
|
|
|
|
|
.alignItems(VerticalAlign.Center)
|
|
|
|
|
|
|
|
|
|
|
|
if (!isLast) {
|
|
|
|
|
|
Divider()
|
|
|
|
|
|
.color(FbColors.line2)
|
|
|
|
|
|
.margin({ left: 44 })
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
2026-11-05 13:43:03 +08:00
|
|
|
|
.onClick((): void => {
|
|
|
|
|
|
this.openMovieDetail(m.id, m);
|
|
|
|
|
|
})
|
2026-10-24 20:22:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 观影行的副标题:`2026-10-20 · 万达影城`(影院名空则只留日期) */
|
|
|
|
|
|
private movieSub(m: FbMovieListItem): string {
|
|
|
|
|
|
const d: string = this.memoryDate(m.startsAt);
|
|
|
|
|
|
if (m.cinemaName !== '') {
|
|
|
|
|
|
return d + ' · ' + m.cinemaName;
|
|
|
|
|
|
}
|
|
|
|
|
|
return d;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 影院色 → 那个小色块的底色。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 影院色是**六位 hex**(`#RRGGBB`)⇒ 直接拿来做背景会太浓(一眼抢走视线),
|
|
|
|
|
|
* 所以拼一个很浅的透明度版本。用 `rgba` 而不是 `#AARRGGBB`:
|
|
|
|
|
|
* ArkUI 两种都认,但 `rgba` 可读性好得多,也便于这里做兜底。
|
|
|
|
|
|
* ⚠️ 非法值(空串 / 不是 `#RRGGBB`)一律退回中性灰 —— 否则 `rgba(#zzz, .12)`
|
|
|
|
|
|
* 会被解析成透明,色块直接消失(看着像"这一行没图标")。
|
|
|
|
|
|
*/
|
|
|
|
|
|
private movieTint(hex: string): string {
|
|
|
|
|
|
const m: RegExpMatchArray | null = hex.match(/^#[0-9a-fA-F]{6}$/);
|
|
|
|
|
|
if (m === null) {
|
|
|
|
|
|
return '#F1EDE7';
|
|
|
|
|
|
}
|
|
|
|
|
|
const r: number = parseInt(hex.substring(1, 3), 16);
|
|
|
|
|
|
const g: number = parseInt(hex.substring(3, 5), 16);
|
|
|
|
|
|
const b: number = parseInt(hex.substring(5, 7), 16);
|
|
|
|
|
|
return 'rgba(' + r.toString() + ',' + g.toString() + ',' + b.toString() + ',0.12)';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 「联系人」里的一个小格子(v86):头像/首字 + 称呼,三列排。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 宽度写 **33.33%** 而不是固定 vp:卡片宽度会随屏幕变(不写死),
|
|
|
|
|
|
* 固定宽度在大屏上会留一大片空、在小屏上会溢出。
|
|
|
|
|
|
* ⚠️ 内层再套一层 `padding` 做**呼吸**:三列贴死会显得挤成一团。
|
|
|
|
|
|
*/
|
|
|
|
|
|
@Builder
|
|
|
|
|
|
personTile(p: FbRosterPerson) {
|
|
|
|
|
|
Column({ space: 5 }) {
|
|
|
|
|
|
if (p.avatar !== '') {
|
|
|
|
|
|
Image(p.avatar)
|
|
|
|
|
|
.width(38)
|
|
|
|
|
|
.height(38)
|
|
|
|
|
|
.borderRadius(19)
|
|
|
|
|
|
.objectFit(ImageFit.Cover)
|
|
|
|
|
|
.alt($r('app.media.fb_ic_people'))
|
|
|
|
|
|
} else {
|
|
|
|
|
|
/* 没有头像 ⇒ 首字 + 名册里那个人的底色(与名册页一致,不另算颜色)。
|
|
|
|
|
|
⚠️ 字也要用**对色**(`FbPersonRepo` 存了 avatarColor 是给底用的)——
|
|
|
|
|
|
这里简单用白字:底色本身就是中深色(名册页的取色规则)。 */
|
|
|
|
|
|
Text(this.personInitial(p))
|
|
|
|
|
|
.width(38)
|
|
|
|
|
|
.height(38)
|
|
|
|
|
|
.borderRadius(19)
|
|
|
|
|
|
.backgroundColor(p.avatarColor !== '' ? p.avatarColor : '#B7A99A')
|
|
|
|
|
|
.fontColor(Color.White)
|
|
|
|
|
|
.fontSize(16)
|
|
|
|
|
|
.fontWeight(FontWeight.Medium)
|
|
|
|
|
|
.textAlign(TextAlign.Center)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Text(this.personLabel(p))
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(FbColors.ink)
|
|
|
|
|
|
.maxLines(1)
|
|
|
|
|
|
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.textAlign(TextAlign.Center)
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('33.33%')
|
|
|
|
|
|
.padding({ top: 8, bottom: 8 })
|
|
|
|
|
|
.alignItems(HorizontalAlign.Center)
|
2026-11-05 13:43:03 +08:00
|
|
|
|
.onClick((): void => {
|
|
|
|
|
|
this.openPersonDetail(p);
|
|
|
|
|
|
})
|
2026-10-24 20:22:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 名册里那个人的显示名:**昵称优先**(`shown`),退回本名。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 与名册页同一个口径(`shown` 就是"我怎么叫他")。
|
|
|
|
|
|
* 直接用 `name`(本名)会与名册页显示得不一样,
|
|
|
|
|
|
* 用户会觉得"首页那个人我不认识"。
|
|
|
|
|
|
*/
|
|
|
|
|
|
private personLabel(p: FbRosterPerson): string {
|
|
|
|
|
|
if (p.shown !== '') {
|
|
|
|
|
|
return p.shown;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (p.label !== '') {
|
|
|
|
|
|
return p.label;
|
|
|
|
|
|
}
|
|
|
|
|
|
return p.name;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 首字(头像兜底)。空名兜底成 `?`,别画出一个空圆。 */
|
|
|
|
|
|
private personInitial(p: FbRosterPerson): string {
|
|
|
|
|
|
const nm: string = this.personLabel(p);
|
|
|
|
|
|
return nm.length > 0 ? nm.substring(0, 1) : '?';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-18 20:14:40 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 「最近纪念日」里的一行(v59,用户 2026-10-18)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 形态:**倒计时徽标**(`还有 12 天`)+ 标题 + 日期/周岁。
|
|
|
|
|
|
* 用户要的就是这个倒计时:「距离哪个纪念日还有多少天,
|
|
|
|
|
|
* 距离谁谁谁的生日还有多少天」—— `title` 里已经带了姓名
|
|
|
|
|
|
* (服务端拼的「老杨 的生日」),所以一句话读完就是用户要的表达。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 与 `memoryRow` 同一个反模式提醒:这里也是**两参数 @Builder**。
|
|
|
|
|
|
* 它安全的前提是"整块由 ForEach 按 key 重建"(数据一次性到齐)——
|
|
|
|
|
|
* 若将来出现"数据变了但行文字没变",**先改成组件**(如 `FbStatCell`),
|
|
|
|
|
|
* 别在这个 Builder 上打补丁。
|
|
|
|
|
|
*/
|
|
|
|
|
|
@Builder
|
|
|
|
|
|
recentDayRow(d: FbRecentDay, isLast: boolean) {
|
|
|
|
|
|
Column({ space: 0 }) {
|
|
|
|
|
|
Row({ space: 10 }) {
|
|
|
|
|
|
// ---- 倒计时徽标:把"还剩几天"做成一眼可见的那一块 ----
|
|
|
|
|
|
// 今天那种用实底强调(它是"就是今天",比"还有 N 天"更该被看见)
|
|
|
|
|
|
Text(this.countdownText(d.days))
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontWeight(d.days === 0 ? FontWeight.Medium : FontWeight.Normal)
|
|
|
|
|
|
.fontColor(d.days === 0 ? Color.White : this.recentTint(d.source))
|
|
|
|
|
|
.backgroundColor(d.days === 0 ? this.recentTint(d.source) : this.recentWash(d.source))
|
|
|
|
|
|
.borderRadius(FbMetric.radiusPill)
|
|
|
|
|
|
.padding({ left: 9, right: 9, top: 3, bottom: 3 })
|
|
|
|
|
|
.textAlign(TextAlign.Center)
|
|
|
|
|
|
|
|
|
|
|
|
// ---- 标题(已含姓名 / 纪念日名)----
|
|
|
|
|
|
Text(d.title)
|
|
|
|
|
|
.fontSize(FbMetric.fontSmall)
|
|
|
|
|
|
.fontColor(FbColors.ink)
|
|
|
|
|
|
.layoutWeight(1)
|
|
|
|
|
|
.maxLines(1)
|
|
|
|
|
|
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
|
|
|
|
|
|
|
|
|
|
|
// ---- 右侧:日期(+ 周岁 / 第几个,有才显示)----
|
|
|
|
|
|
Text(this.recentMeta(d))
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(FbColors.faint)
|
|
|
|
|
|
.maxLines(1)
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding({ top: 11, bottom: 11 })
|
|
|
|
|
|
.alignItems(VerticalAlign.Center)
|
|
|
|
|
|
|
|
|
|
|
|
if (!isLast) {
|
|
|
|
|
|
Divider()
|
|
|
|
|
|
.color(FbColors.line2)
|
|
|
|
|
|
.margin({ left: 18 })
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
2026-11-05 13:43:03 +08:00
|
|
|
|
.onClick((): void => {
|
|
|
|
|
|
this.openDayDetail(d);
|
|
|
|
|
|
})
|
2026-10-18 20:14:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 倒计时文案。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ `days === 0` 必须说「就是今天」而不是「还有 0 天」——
|
|
|
|
|
|
* 后者读起来像"已经过了",正是最容易搞反的地方。
|
|
|
|
|
|
*/
|
|
|
|
|
|
private countdownText(days: number): string {
|
|
|
|
|
|
if (days <= 0) {
|
|
|
|
|
|
return '就是今天';
|
|
|
|
|
|
}
|
|
|
|
|
|
return '还有 ' + days.toString() + ' 天';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 右侧那行小字:日期 +(周岁 / 第几个)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* · 日期一律显示(`MM-DD`,年份对"年年过"的纪念日没有信息量);
|
|
|
|
|
|
* · `age >= 0` ⇒ 生日,附「N 岁」(0 岁是**真实存在**的,不能当"没有");
|
|
|
|
|
|
* · `nth >= 1` ⇒ 用户填了起始年,附「第 N 个」。
|
|
|
|
|
|
*/
|
|
|
|
|
|
private recentMeta(d: FbRecentDay): string {
|
|
|
|
|
|
const md: string = d.date.length >= 10 ? d.date.substring(5, 10) : d.date;
|
|
|
|
|
|
const parts: string[] = [md];
|
|
|
|
|
|
if (d.age >= 0) {
|
|
|
|
|
|
parts.push(d.age.toString() + ' 岁');
|
|
|
|
|
|
}
|
|
|
|
|
|
if (d.nth >= 1) {
|
|
|
|
|
|
parts.push('第 ' + d.nth.toString() + ' 个');
|
|
|
|
|
|
}
|
|
|
|
|
|
return parts.join(' · ');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 纪念日的强调色:生日用粉、其余用紫(与网页版 `hb-mark` 的 birthday/anniv 一致) */
|
|
|
|
|
|
private recentTint(source: string): string {
|
|
|
|
|
|
return this.isBirthday(source) ? '#A03060' : '#6B4A86';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 对应的极浅底(与上面同一档色的淡化) */
|
|
|
|
|
|
private recentWash(source: string): string {
|
|
|
|
|
|
return this.isBirthday(source) ? '#FDECF3' : '#F2ECF7';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 是不是"生日"那类(服务端派生的生日条目;`custom` 是用户自己填的纪念日) */
|
|
|
|
|
|
private isBirthday(source: string): boolean {
|
|
|
|
|
|
return source === 'birthday_solar' || source === 'birthday_lunar';
|
|
|
|
|
|
}
|
2026-10-11 22:07:48 +08:00
|
|
|
|
}
|