v0.7.0 接上真实服务器:登录 / 设备会话 / 全量同步与各模块真实数据
从「演示数据壳子」走到「真的能连家里的服务器用」。
登录(用户 2026-10-17「完整的登录功能」)
· 首次启动先填服务器地址 → /api/v1/ping 自报门户(认得出"这地址上真是家书")
· 手机号或用户名 + 密码 → 拿令牌;失败提示统一,不泄露账号是否存在
· FbDeviceStore:首次启动生成并持久化设备 UUID(三方应用拿不到 UDID),
设备名取机型(marketName),并上报 platform / app_version
设备会话(v57,用户 2026-10-17)
· 一把令牌 = 一台设备的一条会话;每次启动打一次 /me(有网就验)
· 两种锁分开:need_login(401/被踢 → 清令牌)与 offline_expired
(离线超 24h → 只锁界面、不清数据、联网自动恢复)——
混成一种就会违背用户要的"一直联网就不用每天输密码"
· 运行期任何请求 401 → 网络层全局回调立刻锁定跳登录页(FbHttp 的 authProbe
让登录/校验类请求跳过,避免"两个地方同时跳登录页")
· 退出登录:先清本地令牌(主防线),再尽力通知服务端
· 设置页显示版本号与「这台设备」
数据层
· FbDb / FbSchema / FbParse / 各 Repo(日程 / 名册 / 观影 / 模块)+ FbSyncAll
· 首页改为服务端 /home(农历/节气/纪念日/生日/天气/统计/往年的今天)
· 日程页用 FbDayEventPanel,与网页端显示方式/红线/并排三处一致
验证:ArkTS 静态守卫 231/0;hvigor 编译 BUILD SUCCESSFUL(entry-default-signed.hap)。
This commit is contained in:
@@ -1,25 +1,53 @@
|
||||
/**
|
||||
* 家书 FamilyBook · 首页(鸿蒙端)
|
||||
*
|
||||
* 对应 Web 端 `templates/home.php`。版面照搬它的三段:
|
||||
* ① hero(家庭名 + 格言 + 我的资料/家谱两个快捷入口)
|
||||
* ② 信息面板(问候 + 日期/星期/农历 + 日子标记 | 右侧天气)
|
||||
* ② 今天的日程 + 近期任务(两块并排;窄屏上下堆叠)
|
||||
* ④ 「往年的今天」回忆条
|
||||
* ⑤ 模块入口卡片(12 个模块的关键数字)
|
||||
* 对应 Web 端 `templates/home.php`。版面:
|
||||
* ① 个人信息卡(问候 + 姓名 + 日期/星期/农历 + 日子标记 + 天气 + 近七天数字)
|
||||
* ② 今天的日程(🔴 **直接用日程页那块面板组件** —— 用户 2026-10-16)
|
||||
* ③ 近期任务(进行中 + 待办,进行中在前)
|
||||
* ④ 往年的今天(用户 2026-10-17 要求加,与电脑端同一个逻辑)
|
||||
*
|
||||
* ⚠️ 只做**界面**,不做交互实现:卡片点了切模块(前端状态),不发请求。
|
||||
* 🔴🔴 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` 字段,这里加第 ④ 块。
|
||||
*/
|
||||
|
||||
import { FbColors, FbMetric, fbPalette } from '../../common/Theme';
|
||||
import { FbColors, FbMetric } from '../../common/Theme';
|
||||
import { FbAppState, fbAppState } from '../../common/Store';
|
||||
import { FbModuleCard, FbCalEvent, FbTaskItem, FbKinChip } from '../../model/Types';
|
||||
import {
|
||||
FB_DEMO_EVENTS_TODAY, FB_DEMO_EVENTS_SOON, FB_DEMO_KIN, FB_DEMO_MARKS,
|
||||
FB_DEMO_MODULE_CARDS, FB_DEMO_TASKS, FB_DEMO_WEATHER, FB_DEMO_WEEK_STAT
|
||||
} from '../../model/DemoData';
|
||||
import { FbAvatar, FbDot, FbDotLine, FbSectionTitle, FbStatTriple, FbTag } from '../../components/Widgets';
|
||||
import { FbCalEvent, FbHomeData, FbMark, FbMemoryItem, FbTaskItem, FbWeather } from '../../model/Types';
|
||||
import { parseHome } from '../../data/FbParse';
|
||||
import { loadModule } from '../../data/FbModuleLoad';
|
||||
import { FB_CAL_SETTINGS, FbCalStyle } from '../../data/FbCalSettings';
|
||||
import { FbSectionTitle } from '../../components/Widgets';
|
||||
import { FbStatCell } from '../../components/FbStatCell';
|
||||
import { FbDayEventPanel } from '../../components/FbDayEventPanel';
|
||||
import { FB_DOCK_RESERVE } from '../../components/FloatDock';
|
||||
import { fbOnScroll, fbOnTouch } from '../../common/FbScroll';
|
||||
|
||||
@ComponentV2
|
||||
export struct FbHomePage {
|
||||
@@ -31,10 +59,81 @@ export struct FbHomePage {
|
||||
@BuilderParam topBar: () => void;
|
||||
@Param app: FbAppState = fbAppState();
|
||||
|
||||
private cards: FbModuleCard[] = FB_DEMO_MODULE_CARDS;
|
||||
private todayEvents: FbCalEvent[] = FB_DEMO_EVENTS_TODAY;
|
||||
private soonEvents: FbCalEvent[] = FB_DEMO_EVENTS_SOON;
|
||||
private tasks: FbTaskItem[] = FB_DEMO_TASKS;
|
||||
// ---------------- 数据(全部来自 `/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[] = [];
|
||||
/** 顶部提示(同步失败等) */
|
||||
@Local syncNote: string = '';
|
||||
/**
|
||||
* 日程显示方式(时间轴 / 列表)—— 与日程页**读同一个设置**。
|
||||
* ⚠️ 首页显示"今天的日程",用户在哪一处把显示方式调了,另一处也该跟着变
|
||||
* (它们本来就是同一份数据的不同入口)。
|
||||
*/
|
||||
@Local calStyle: FbCalStyle = 'timeline';
|
||||
|
||||
aboutToAppear(): void {
|
||||
// 显示方式来自日程设置(启动时已预载进内存,可同步读)
|
||||
this.calStyle = FB_CAL_SETTINGS.getStyle();
|
||||
this.load();
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉首页数据。
|
||||
*
|
||||
* ⚠️ 首页**只要一个接口**了(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;
|
||||
},
|
||||
(note: string): void => {
|
||||
this.syncNote = note;
|
||||
});
|
||||
}
|
||||
|
||||
/** 问候语时段(与 Web 端 home.php 里那段 PHP 同口径) */
|
||||
private hello(): string {
|
||||
@@ -54,24 +153,81 @@ export struct FbHomePage {
|
||||
return '晚上好';
|
||||
}
|
||||
|
||||
/** 今天的日期与星期 */
|
||||
/** 日期行:`10 月 16 日 · 星期五`(服务端给的字段;没有就本地算兜底) */
|
||||
private dateLine(): string {
|
||||
if (this.todayLabel !== '') {
|
||||
return this.weekday !== '' ? (this.todayLabel + ' · ' + this.weekday) : this.todayLabel;
|
||||
}
|
||||
const d: Date = new Date();
|
||||
const week: string[] = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
|
||||
return (d.getMonth() + 1).toString() + ' 月 ' + d.getDate().toString() + ' 日 · '
|
||||
+ week[d.getDay()];
|
||||
const w: string[] = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
|
||||
return (d.getMonth() + 1).toString() + ' 月 ' + d.getDate().toString() + ' 日 · ' + w[d.getDay()];
|
||||
}
|
||||
|
||||
/** 天气图标(按文案粗略映射到已有的几个图标上,不引外部图) */
|
||||
private weatherIcon(): Resource {
|
||||
// TODO(接口):真实天气码 → 图标映射。现在只区分「有太阳/没太阳」两种。
|
||||
return this.weather().text.indexOf('晴') >= 0
|
||||
? $r('app.media.fb_ic_days')
|
||||
: $r('app.media.fb_ic_calendar');
|
||||
/** 天气图标颜色(按服务端给的 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';
|
||||
}
|
||||
|
||||
private weather() {
|
||||
return FB_DEMO_WEATHER;
|
||||
/**
|
||||
* 日子标记的配色(与网页版 `.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();
|
||||
}
|
||||
|
||||
build() {
|
||||
@@ -79,7 +235,6 @@ export struct FbHomePage {
|
||||
Column({ space: 0 }) {
|
||||
this.topBar()
|
||||
|
||||
|
||||
// ==================== ① 个人信息卡(问候 + 今天 + 天气)====================
|
||||
// 🔴 用户 2026-10-12 定的:这一块**要白色卡片**("这样显得好看")。
|
||||
// ⚠️ 曾一度做成"不套卡片的页面头部",理由是省首屏高度;
|
||||
@@ -107,72 +262,100 @@ export struct FbHomePage {
|
||||
Text(this.dateLine())
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.muted)
|
||||
Text('八月廿二')
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.faint)
|
||||
// 🔴 农历:真实值(接口给)。算不出时**不显示**,不编一个
|
||||
if (this.lunarShort !== '') {
|
||||
Text(this.lunarShort)
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
}
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
|
||||
// 天气:靠右,温度大字 + 文案小字
|
||||
Row({ space: 6 }) {
|
||||
Image(this.weatherIcon())
|
||||
.width(26)
|
||||
.height(26)
|
||||
.renderMode(ImageRenderMode.Template)
|
||||
.fillColor(FbColors.brand2)
|
||||
Column({ space: 0 }) {
|
||||
Text(this.weather().temp.toString() + '°')
|
||||
.fontSize(24)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
Text(this.weather().text)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.muted)
|
||||
.maxLines(1)
|
||||
// ⚠️ 没配地点 / 取不到天气时**整块不画**(与网页版一致)——
|
||||
// 画一个"——"占位反而让人以为坏了。
|
||||
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)
|
||||
}
|
||||
.alignItems(HorizontalAlign.End)
|
||||
.alignItems(VerticalAlign.Center)
|
||||
}
|
||||
.alignItems(VerticalAlign.Center)
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(VerticalAlign.Top)
|
||||
|
||||
// 第二行:天气地点 + 日子标记(节气 / 节日 / 生日)
|
||||
// 第二行:天气地点 + 日子标记(节气 / 节日 / 纪念日 / 生日)
|
||||
// 🔴🔴 这里**绝不能**再出现 `FB_DOCK_RESERVE` 的 padding ——
|
||||
// 它 96vp,接在这会把这个小方块撑成一大片空白(v54 修的那个"概述块太高")。
|
||||
Column({ space: 6 }) {
|
||||
Text(this.weather().place + ' · ' + this.weather().wind)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
.maxLines(1)
|
||||
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)
|
||||
}
|
||||
.width('100%')
|
||||
|
||||
Flex({ wrap: FlexWrap.Wrap }) {
|
||||
ForEach(FB_DEMO_MARKS, (mk: string, idx: number) => {
|
||||
FbTag({ text: mk, tone: idx === 0 ? 'green' : 'brand' })
|
||||
.margin({ right: 6, top: 2 })
|
||||
}, (mk: string, idx: number): string => 'mk_' + idx.toString() + '_' + mk)
|
||||
}
|
||||
.width('100%')
|
||||
// 底部悬浮快捷栏的预留:它浮在内容之上,不留白会压住最后一屏
|
||||
.padding({ bottom: FB_DOCK_RESERVE })
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
|
||||
// 第三行:三个数字(未来七天日程 / 待办 / 逾期)——
|
||||
// 贴在卡片底部,作为"今天要不要操心的快速判断"
|
||||
// (卡片内用浅分隔线分开,视觉上比留白更"有层次")
|
||||
// 第三行:四个数字(**与网页版 `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**(同样是按值传递)。
|
||||
Divider().color(FbColors.line2)
|
||||
FbStatTriple({
|
||||
labelA: '未来七天',
|
||||
valueA: FB_DEMO_WEEK_STAT.events.toString(),
|
||||
labelB: '待办',
|
||||
valueB: FB_DEMO_WEEK_STAT.todos.toString(),
|
||||
labelC: '逾期',
|
||||
valueC: FB_DEMO_WEEK_STAT.overdue.toString(),
|
||||
cAlarm: FB_DEMO_WEEK_STAT.overdue > 0
|
||||
})
|
||||
Row() {
|
||||
FbStatCell({ value: this.statToday, label: '今天日程' })
|
||||
FbStatCell({ value: this.statDoing, label: '在做' })
|
||||
FbStatCell({ value: this.statTodo, label: '待办' })
|
||||
FbStatCell({ value: this.statOverdue, label: '逾期', alarm: true })
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ top: 10, bottom: 10 })
|
||||
}
|
||||
// 🔴 不要 `.width('100%')` —— 它会先把宽度占满,导致后面的 margin 失效,
|
||||
// 卡片于是**顶到屏幕两侧边缘**(实测 x=0..1080),而别的区块是留边 16vp 的,
|
||||
@@ -189,166 +372,106 @@ export struct FbHomePage {
|
||||
})
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
|
||||
// ==================== ② 今天的日程 + 近期任务 ====================
|
||||
// ==================== ② 今天的日程 ====================
|
||||
// 🔴 用户 2026-10-16:「今天的日程,现在显示的是日程里面的展示内容。
|
||||
// 把**今天那个块**拿到首页上来就行了。」
|
||||
// ⇒ 直接用日程页那块面板组件(月/周/列表三处用的同一个)——
|
||||
// 于是显示方式(时间轴/列表)、冲突横向并排、当前时间红线
|
||||
// 在首页与日程页**完全一致**,不会再出现"两个地方长得不一样"。
|
||||
// ⚠️ 面板自带卡片样式与外边距,这里**不要再套一层卡片**。
|
||||
// ⚠️ `showHead: false` —— 本页已有 `FbSectionTitle`,两个头会重复。
|
||||
// ⚠️ 空态文案也换掉:首页没有"右下角 +"那个按钮。
|
||||
FbSectionTitle({ title: '今天的日程', extra: this.todayEvents.length.toString() + ' 条' })
|
||||
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12 })
|
||||
|
||||
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 => {
|
||||
// 点条目 ⇒ 切到日程模块(首页不做就地编辑:那是日程页的职责)
|
||||
this.app.switchModule('calendar');
|
||||
}
|
||||
})
|
||||
|
||||
// ==================== ③ 近期任务 ====================
|
||||
Column({ space: 0 }) {
|
||||
FbSectionTitle({ title: '今天的日程', extra: this.todayEvents.length.toString() + ' 条' })
|
||||
Column({ space: 10 }) {
|
||||
if (this.todayEvents.length === 0) {
|
||||
FbDotLine({ label: '今天没有安排,过得轻松一点', muted: true })
|
||||
FbSectionTitle({ title: '近期任务', extra: this.taskExtra() })
|
||||
Column({ space: 0 }) {
|
||||
if (this.tasks.length === 0) {
|
||||
Row() {
|
||||
Text('没有进行中的任务,也没有待办。')
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.muted)
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ top: 12, bottom: 12 })
|
||||
} else {
|
||||
ForEach(this.todayEvents, (ev: FbCalEvent) => {
|
||||
Row({ space: 10 }) {
|
||||
Text(ev.allDay ? '全天' : ev.time)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.muted)
|
||||
.width(46)
|
||||
Column() {
|
||||
FbDot({ dia: 8, fillC: ev.color })
|
||||
}
|
||||
.width(8)
|
||||
|
||||
Text(ev.title)
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.ink)
|
||||
.layoutWeight(1)
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
|
||||
Text(ev.calName)
|
||||
.fontSize(10)
|
||||
.fontColor(Color.White)
|
||||
.backgroundColor(ev.color)
|
||||
.borderRadius(4)
|
||||
.padding({ left: 5, right: 5, top: 1, bottom: 1 })
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(VerticalAlign.Center)
|
||||
}, (ev: FbCalEvent): string => 'tev_' + ev.id.toString())
|
||||
ForEach(this.tasks, (t: FbTaskItem, idx: number) => {
|
||||
this.taskRow(t, idx === this.tasks.length - 1)
|
||||
}, (t: FbTaskItem, idx: number): string => 'htk_' + t.id.toString())
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.padding(FbMetric.cardPad)
|
||||
.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: 8 })
|
||||
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12 })
|
||||
|
||||
// ==================== ④ 往年的今天 ====================
|
||||
// 🔴 用户 2026-10-17:「近期任务的下面,应该加上**往年的今天**,
|
||||
// 跟电脑端一样,你看看那个是什么逻辑。」
|
||||
// ⇒ 位置**就在近期任务下面**(用户明确指定,不是"放在首页某处")。
|
||||
// 电脑端的逻辑见 `App\Home::memories()`:只认**往年的同一天**
|
||||
// (不退回同月),来源是「看过的电影」+「走过的**实际行程**」,
|
||||
// 合并按时间倒序取 6 条。
|
||||
// ⚠️ 数据来自 `/home` 的 `memories` —— 与网页版**同一份实现**,
|
||||
// 不要端上自己再查一遍(会出现"网页 2 年前、手机 3 年前")。
|
||||
Column({ space: 0 }) {
|
||||
FbSectionTitle({
|
||||
title: '近期任务',
|
||||
extra: '进行中与已逾期'
|
||||
title: '往年的今天',
|
||||
extra: this.todayLabel !== '' ? this.todayLabel : ''
|
||||
})
|
||||
Column({ space: 10 }) {
|
||||
ForEach(this.recentTasks(), (t: FbTaskItem) => {
|
||||
Row({ space: 10 }) {
|
||||
FbDot({ dia: 8, fillC: t.overdue ? '#C0533F' : t.boardColor })
|
||||
|
||||
Text(t.title)
|
||||
Column({ space: 0 }) {
|
||||
if (this.memories.length === 0) {
|
||||
// 空态与网页版一字不差(两句:一句说没有,一句给个盼头)
|
||||
Column({ space: 6 }) {
|
||||
Text('往年的这一天还没有记录')
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.ink)
|
||||
.layoutWeight(1)
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
|
||||
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 ? FbColors.red : FbColors.faint)
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(VerticalAlign.Center)
|
||||
}, (t: FbTaskItem): string => 'htk_' + t.id.toString())
|
||||
}
|
||||
.width('100%')
|
||||
.padding(FbMetric.cardPad)
|
||||
.backgroundColor(FbColors.surface)
|
||||
.borderRadius(FbMetric.radius)
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12 })
|
||||
|
||||
// ==================== ③ 我的家人 ====================
|
||||
Column({ space: 0 }) {
|
||||
FbSectionTitle({ title: '我的家人', extra: '以我为中心 · 上下三代' })
|
||||
Flex({ wrap: FlexWrap.Wrap }) {
|
||||
ForEach(FB_DEMO_KIN, (k: FbKinChip) => {
|
||||
Column({ space: 4 }) {
|
||||
FbAvatar({ name: k.shown, color: k.avatarColor, dia: 42, fontSizeVp: 17 })
|
||||
Text(k.shown)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.ink)
|
||||
.maxLines(1)
|
||||
Text(k.kinship)
|
||||
.fontSize(10)
|
||||
.fontColor(FbColors.faint)
|
||||
.maxLines(1)
|
||||
}
|
||||
.width(64)
|
||||
.padding({ top: 6, bottom: 6 })
|
||||
}, (k: FbKinChip): string => 'kin_' + k.id.toString())
|
||||
}
|
||||
.width('100%')
|
||||
.padding(FbMetric.cardPad)
|
||||
.backgroundColor(FbColors.surface)
|
||||
.borderRadius(FbMetric.radius)
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12 })
|
||||
|
||||
// ==================== ④ 未来七天 ====================
|
||||
Column({ space: 0 }) {
|
||||
FbSectionTitle({ title: '未来七天', extra: this.soonEvents.length.toString() + ' 条' })
|
||||
Column({ space: 10 }) {
|
||||
ForEach(this.soonEvents, (ev: FbCalEvent) => {
|
||||
Row({ space: 10 }) {
|
||||
Text(ev.date)
|
||||
Text('日子还长,等有了第一次,明年这时候就会自己冒出来。')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.muted)
|
||||
.width(44)
|
||||
Text(ev.allDay ? '全天' : ev.time)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
.width(40)
|
||||
FbDot({ dia: 7, fillC: ev.color })
|
||||
Text(ev.title)
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.ink)
|
||||
.layoutWeight(1)
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(VerticalAlign.Center)
|
||||
}, (ev: FbCalEvent): string => 'sev_' + ev.id.toString())
|
||||
.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())
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.padding(FbMetric.cardPad)
|
||||
.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 })
|
||||
|
||||
// ==================== ⑤ 模块入口 ====================
|
||||
Column({ space: 0 }) {
|
||||
FbSectionTitle({ title: '全部模块', extra: this.cards.length.toString() + ' 个' })
|
||||
|
||||
Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
|
||||
ForEach(this.cards, (c: FbModuleCard) => {
|
||||
this.moduleCard(c)
|
||||
}, (c: FbModuleCard): string => 'mc_' + c.key)
|
||||
}
|
||||
.width('100%')
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12, bottom: 28 })
|
||||
// 🔴🔴 **底部悬浮快捷栏的预留** —— 它必须留在**页面最根部**。
|
||||
// v54 之前它被接在了概述卡中间那个小 Flex 上(96vp 凭空撑高卡片),
|
||||
// 真正的页面底部反而没有留白、被悬浮栏压住最后一屏。
|
||||
// ⚠️ 别再把它挪到任何中间区块上。
|
||||
Blank().height(FB_DOCK_RESERVE)
|
||||
}
|
||||
.width('100%')
|
||||
}
|
||||
@@ -356,72 +479,168 @@ export struct FbHomePage {
|
||||
.height('100%')
|
||||
.scrollBar(BarState.Off)
|
||||
.edgeEffect(EdgeEffect.Spring)
|
||||
// 🔴 滚动方向上报(驱动底部悬浮栏随滚收起 / 唤回)—— 见 common/FbScroll.ets
|
||||
.onDidScroll(fbOnScroll)
|
||||
|
||||
// 🔴 触摸方向兜底(内容不足一屏的页面滚不动,只能靠手指位移判断)
|
||||
// 与上面的 `onDidScroll` 互补、互不冲突 —— 见 common/FbScroll.ets
|
||||
.onTouch(fbOnTouch)
|
||||
.align(Alignment.TopStart)
|
||||
}
|
||||
|
||||
/** 一块模块入口卡(图标 + 模块名 + 一句关键数字) */
|
||||
/** 天气地点行:`青岛市 · 崂山区 · 东南风 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) {
|
||||
return doing.toString() + ' 件在做 · ' + todo.toString() + ' 件待办';
|
||||
}
|
||||
return doing.toString() + ' 件在做';
|
||||
}
|
||||
|
||||
/**
|
||||
* 一条任务(首页版)。
|
||||
*
|
||||
* 🔴 与网页版首页同一套信息:圆点(看板色,逾期变红)+ 标题 + **状态词**
|
||||
* (用户第 3 条:「除了显示任务的标题之外,还要显示他的状态」)
|
||||
* + 子任务进度 + 截止时间。
|
||||
* ⚠️ 看板色**必须**用真实派生色(`boardColor`)—— 用户 2026-10-08 报过
|
||||
* 「首页近期任务……颜色并没有跟看板颜色一致」。
|
||||
*/
|
||||
@Builder
|
||||
moduleCard(c: FbModuleCard) {
|
||||
Column({ space: 8 }) {
|
||||
Row({ space: 8 }) {
|
||||
Image(this.cardIcon(c.key))
|
||||
.width(18)
|
||||
.height(18)
|
||||
.renderMode(ImageRenderMode.Template)
|
||||
.fillColor(fbPalette(c.color).brand)
|
||||
Text(c.name)
|
||||
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 条要求
|
||||
Text(t.status === 'doing' ? '进行中' : '待办')
|
||||
.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)
|
||||
|
||||
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)
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
.layoutWeight(1)
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
|
||||
// 那天的日期(`YYYY-MM-DD`)
|
||||
Text(this.memoryDate(m.startsAt))
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ top: 11, bottom: 11 })
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
Text(c.big)
|
||||
.fontSize(20)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(fbPalette(c.color).brand)
|
||||
.maxLines(1)
|
||||
|
||||
Text(c.lines.length > 0 ? c.lines[0] : '')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
if (!isLast) {
|
||||
Divider()
|
||||
.color(FbColors.line2)
|
||||
.margin({ left: 18 })
|
||||
}
|
||||
}
|
||||
.width('48.5%')
|
||||
.padding(14)
|
||||
.backgroundColor(FbColors.surface)
|
||||
.borderRadius(FbMetric.radius)
|
||||
.margin({ bottom: 10 })
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.onClick(() => {
|
||||
this.app.switchModule(c.key);
|
||||
})
|
||||
.width('100%')
|
||||
}
|
||||
|
||||
/** 卡片图标(按模块 key 取) */
|
||||
private cardIcon(key: string): Resource {
|
||||
switch (key) {
|
||||
case 'people': return $r('app.media.fb_ic_people');
|
||||
case 'kin': return $r('app.media.fb_ic_kin');
|
||||
case 'movies': return $r('app.media.fb_ic_movies');
|
||||
case 'trips': return $r('app.media.fb_ic_trips');
|
||||
case 'calendar': return $r('app.media.fb_ic_calendar');
|
||||
case 'resources': return $r('app.media.fb_ic_resources');
|
||||
case 'tasks': return $r('app.media.fb_ic_tasks');
|
||||
case 'days': return $r('app.media.fb_ic_days');
|
||||
case 'ledger': return $r('app.media.fb_ic_ledger');
|
||||
case 'books': return $r('app.media.fb_ic_books');
|
||||
case 'notes': return $r('app.media.fb_ic_notes');
|
||||
default: return $r('app.media.fb_ic_home');
|
||||
}
|
||||
}
|
||||
|
||||
/** 首页只放「进行中 + 已逾期」两类任务(与 Web 端同一取舍) */
|
||||
private recentTasks(): FbTaskItem[] {
|
||||
return this.tasks.filter((t: FbTaskItem): boolean => t.status === 'doing' || t.overdue);
|
||||
/** `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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user