v0.8.1 界面六条:删离线入口 / 标语统一 / 最近纪念日 / 图标真上色 / 真头像 / 日程四修

用户 2026-10-18 六条(全部已在真机逐条验证):

① 登录页「先不登录 / 离线使用」彻底删除
   用户:「把那些不登录、看离线数据之类的提示全部去掉。以后就不再拥有这样的
   功能了,把这些功能也全部删除。」
   ⇒ 两个按钮 + onSkip + allowSkip 参数 + Index 里的 skip 分支一起删净。
   这是产品决定(不是隐藏):该路径本身就是安全缺口,v58 上了整库加密后更要堵死。

② 标语统一为「一家人用岁月写成的一本书」
   用户:「这也是我们"家书"这个名字的由来。所以你不要再乱换这个标语了,
   只要是想出现标语或者副标题的地方,就用这样一句话。」
   · 端上 FB_DEFAULT_SLOGAN 是唯一定义处(登录页副标题 + 抽屉回落都用它);
   · 抽屉左上角原来是家庭名,现在显示**标语**;
   · 三层优先级(个人 slogan → 家庭 motto → 应用默认口号)**在服务端算**
     (Family::slogan,与网页端首页 heroSlogan 同一套),/login 与 /me 都返回。
   用户还说:「如果这个用户自己有标语……这个地方就是用户自己自定义的标语。」
   真机实测:登录者杨永全自己写了「修身,齐家」⇒ 抽屉就显示他那句。

③ 首页新增「最近纪念日」(放在近期任务之后、往年的今天之前)
   用户:「未来最近的三个纪念日,包括每个人的生日,以及手动填写的各个纪念日。
   以倒计时形式展示:距离哪个纪念日还有多少天,距离谁谁谁的生日还有多少天。」
   · /home.recent_days = Anniversary::upcoming(fam,pid,370) 取前 3 ——
     取满一年:窗口太小会让"刚过完生日"的家庭一个都取不到,模块直接空着;
   · 数据源 all() = 派生生日 + 自建纪念日,正好覆盖用户说的两样;
   · 倒计时:0 天显示「就是今天」(写「还有 0 天」读起来像已经过了)。

④ 悬浮栏图标也上色(用户:「除了下面的汉字,上面图标的颜色也变成跟汉字一样的颜色」)
   🔴🔴 真根因两层,都是"无报错失效":
   · 第一层:Image.fillColor() **只对 SVG 生效**,而本工程 30+ 图标**全是 PNG**
     ⇒ 全工程 24 处图标染色一直是空操作(文字走 fontColor 所以只有文字变了)。
   · 第二层(真机才现形):改用 colorFilter 后,矩阵写成了"系数乘 RGB"
     (R' = R_原 × 系数) —— 对白色图标成立,但本工程图标是**纯黑**,
     黑 × 任何系数 = 仍是黑 ⇒ 真机上图标像素依旧是 (0,0,0)。
   ⇒ 最终形态:`fbTint` 用 **alpha 当载体**(R' = (r/255) × A_原):
     系数是归一化约定(官方灰度示例系数和为 1,量纲确定);
     形状完全由原图 alpha 决定 ⇒ 黑/白图标都成立,描边抗锯齿自然过渡。
   真机像素实测:日程项图标与文字同为 (176,120,64)≈#B0763A;
   未选中项图标 (44,39,35) 深灰(不再是纯黑)。
   ⚠️ 全工程 24 处 fillColor 一并改掉;FbIconButton.tint 类型改成 string,
      让"误传 Resource"变成编译错误而不是又一个静默失效。

⑤ 顶栏:真实头像 + 拉开按钮间距
   用户:「右上角的头像应该是真实的……现在提醒的按钮距离头像的按钮太近了。」
   · /home 给 me_avatar、/login 给 avatar_url(avatar_url() 按线上存储驱动拼好);
   · 三颗按钮(同步 / 铃铛 / 头像)统一用 FbMetric.gapTopActions = 8。

⑥ 日程四处
   · 「日期下面有几个点」⇒ 月视图与周视图的圆点全部删除,
     连 FbDot / dots() / count / dotColor 一起清(不留死代码);
   · 「农历全部都是初一」⇒ 真根因是端上 lunarOf() **硬编码 return '初一'**,
     而服务端 lunarRange() 本来就每天给真实农历(title)⇒ 改取 title;
   · 「列表这个视图,今天的数据缺一条」⇒ 服务端 /calendar 的 from/to **默认只给本月**,
     而列表视图要显示 -7 ~ +60 天 ⇒ 端上显式传范围
     (loadModule 新增 pathOverride 参数);
   · 「点击了某一天之后并不明显,应该把这一天的颜色变深,而今天用一个较浅的颜色」
     ⇒ 照 SyncCalendar:选中 = 深色实底白字、今天 = 浅色底。
       真机实测:17 号(选中)bg=#FFB0763A,18 号(今天)bg=#FFF5F0EA。

另:FbKeyStore 补上一个真机抓到的 HUKS 坑 —— isKeyItemExist 在密钥不存在时
**会抛 12000011**(不是返回 false),首次运行必被误判成"本机不支持 HUKS"
⇒ 永久明文降级。改成把"不存在"当正常分支,且降级态每次启动重试一次
(把同一把 DEK 重新用 KEK 包起来,不换密钥以免重下全部附件)。

验证:hvigor BUILD SUCCESSFUL;_fb_arkts_guard.js 328/0;
_fb_v59ui_falsify.py 19 项注入 23/0;_fb_home_api.py 60/0;
六条已在真机逐条取像素/布局树确认。
This commit is contained in:
2026-10-18 20:14:40 +08:00
parent 2132ee42d2
commit f020a671c0
27 changed files with 862 additions and 221 deletions
@@ -23,7 +23,7 @@
* 旧代码从条目里臆造 id,导致新建日程根本落不到正确的本上(见 `FbCalRepo.fetchBooks`)。
*/
import { FbColors, FbMetric, fbPalette } from '../../common/Theme';
import { FbColors, FbMetric, fbPalette, FbTint } from '../../common/Theme';
import { FbAppState, fbAppState } from '../../common/Store';
import { FbCalBook, FbCalEvent, isEditableEvent } from '../../model/Types';
import { parseCalendar } from '../../data/FbParse';
@@ -31,7 +31,7 @@ import { loadModule } from '../../data/FbModuleLoad';
import { FbCalRepo, FbCalEventFull } from '../../data/FbCalRepo';
import { FB_CAL_SETTINGS, FbCalView, FbCalStyle } from '../../data/FbCalSettings';
import { FB_TOKEN_STORE } from '../../data/FbTokenStore';
import { FbDot, FbEmptyBox, FbIconButton } from '../../components/Widgets';
import { FbEmptyBox, FbIconButton } from '../../components/Widgets';
import { FbEventSheet } from '../../components/FbEventSheet';
import { FbEventEditSheet } from '../../components/FbEventEditSheet';
import { FbCalSettingsSheet } from '../../components/FbCalSettingsSheet';
@@ -40,7 +40,16 @@ import { FB_DOCK_RESERVE } from '../../components/FloatDock';
import { fbOnScroll, fbOnTouch } from '../../common/FbScroll';
/** 月历上「非本月」日期点的灰(hex 常量:格子数据结构里存的是字符串色,不能用 Resource) */
const FB_FAINT: string = '#A8988A';
/**
* 列表(和月历)要覆盖的日期范围:今天 -7 ~ 今天 +60 天。
*
* 🔴 这两个数**必须**同时用于两处(改一处就得改另一处):
* ① `load()` 里发给服务端的 `from`/`to`(不传的话服务端只给本月 ⇒ 跨月缺数据);
* ② `agendaDays()` 的循环边界(页面实际渲染哪些天)。
* 分开写死就会变成"请求了 A 段、显示 B 段"——那正是本 bug 最容易复发的形态。
*/
const AGENDA_BACK_DAYS: number = 7;
const AGENDA_FWD_DAYS: number = 60;
/** 月历格子(一个日期) */
interface FbCalCell {
@@ -52,10 +61,10 @@ interface FbCalCell {
inMonth: boolean;
/** 是否今天 */
isToday: boolean;
/** 该日的条目数(0 不显示点) */
count: number;
/** 该日第一个条目的色(画小点用) */
dotColor: string;
/* ⚠️ v59:原来的 `count` / `dotColor` 已删除 —— 它们只服务于"日期下面那排
小圆点",而用户 2026-10-18 明确要求**把那排点全部去掉**。
留着这两个字段会让人以为"哪里还在画点",也会让 `cellOf` 白算一遍。 */
/** 农历/节日的短文字(初一显示月名、节日显示节日名) */
lunar: string;
}
@@ -75,7 +84,7 @@ interface FbWeekDay {
/** 一 二 三 … */
w: string;
isToday: boolean;
count: number;
/** ⚠️ v59:原来的 `count`(画小点用)已删 —— 用户要求去掉日期下那排点。 */
lunar: string;
}
@@ -176,6 +185,18 @@ export struct FbCalendarPage {
* ⇒ 拉一次全量、端上按日期筛;切换月份/视图**不再请求**。
*/
private async load(): Promise<void> {
/* 🔴🔴 v59(用户 2026-10-18):「列表这个视图,今天的数据缺一条。」
真根因:服务端 `/calendar` 的 from/to **默认只给本月**
(`date('Y-m-01')` ~ `date('Y-m-t')`),而列表视图要显示 `-7 ~ +60` 天。
跨月那一段服务端压根没返回 ⇒ 落在下月的日程(含"今天往后几天")
在列表里凭空少掉;月视图因为本来就只看本月,所以看不出来。
⇒ 这里**显式传范围**,覆盖列表要用的那一整段(前后各留够余量)。
⚠️ 不要只传"本月",也不要依赖服务端默认值 —— 列表与月视图用的
`allEvents` 是同一份,范围必须**取两者所需的并集**(否则修了列表
又让月视图翻页时缺数据)。
⚠️ 往前 7 天 / 往后 60 天这两个数与 `agendaDays()` 是**同一套口径**,
改一处必须改另一处(分散成两个魔法数字必然漂移)。 */
const range: string = '/api/v1/calendar?from=' + this.agendaFrom() + '&to=' + this.agendaTo();
await loadModule<FbCalEvent[]>('calendar', parseCalendar,
(rows: FbCalEvent[]): void => {
this.allEvents = rows;
@@ -184,7 +205,20 @@ export struct FbCalendarPage {
(note: string): void => {
this.syncNote = note;
this.loading = false;
});
},
range);
}
/** 列表/月历所需范围的起点(今天 - 7 天,`YYYY-MM-DD`) */
private agendaFrom(): string {
return this.isoOfDate(new Date(new Date().getFullYear(), new Date().getMonth(),
new Date().getDate() - AGENDA_BACK_DAYS));
}
/** 列表/月历所需范围的终点(今天 + 60 天,`YYYY-MM-DD`) */
private agendaTo(): string {
return this.isoOfDate(new Date(new Date().getFullYear(), new Date().getMonth(),
new Date().getDate() + AGENDA_FWD_DAYS));
}
// ============================================================
@@ -205,6 +239,11 @@ export struct FbCalendarPage {
return dt.getFullYear().toString() + '-' + this.pad(dt.getMonth() + 1) + '-' + this.pad(dt.getDate());
}
/** `Date` → `YYYY-MM-DD`(服务端 from/to 用的格式) */
private isoOfDate(dt: Date): string {
return this.isoOf(dt.getFullYear(), dt.getMonth() + 1, dt.getDate());
}
/** 只看"可见的日历本"里的条目(设置里能逐本关掉) */
private visibleEvents(): FbCalEvent[] {
if (this.books.length === 0) {
@@ -271,10 +310,22 @@ export struct FbCalendarPage {
return this.shortTitle(e.title);
}
}
// 其次:初一(`kind=lunar` 的条目只在初一有)
// 其次:农历日名
//
// 🔴🔴 2026-10-18 修的真 bug(用户:「农历全部都是初一,说明是错误的数据」):
// 这里原来**硬编码返回 `'初一'`**,注释还写着"`kind=lunar` 的条目只在初一有" ——
// 而事实相反:服务端 `Calendar::month()` 里 `Holiday::lunarRange()` 是
// **每一天都出一条**(`title` = `Lunar::monthName()`(初一显示月名)
// 或 `Lunar::dayName()`),端上把这个真值丢掉了,于是**整月都显示"初一"**。
// ⇒ 直接取服务端给的 title(它本来就是"这一天的农历文字")。
// ⚠️ title 可能带节日后缀(`八月十五 · 中秋`)—— `shortTitle` 截前 3 字,
// 而这张格子里只有 8sp 的位置,本来就该显示短的。
for (const e of this.allEvents) {
if (e.date === iso && e.kind === 'lunar') {
return '初一';
const t: string = this.shortTitle(e.title);
if (t !== '') {
return t;
}
}
}
return '';
@@ -333,25 +384,13 @@ export struct FbCalendarPage {
private cellOf(y: number, m: number, d: number, inMonth: boolean, today: string): FbCalCell {
const iso: string = this.isoOf(y, m, d);
const list: FbCalEvent[] = this.eventsOf(iso);
// 只数"用户日程",不算农历/节气(否则每个格子都有一点)
let n: number = 0;
let color: string = FB_FAINT;
for (const e of list) {
if (e.kind === 'custom' || e.kind === 'anniv') {
if (n === 0) {
color = e.color;
}
n++;
}
}
// ⚠️ v59:这里原来还要遍历一遍 events 去数"有几条"(为画小点用)——
// 那排点已被用户要求删除 ⇒ 这次遍历**整段去掉**(格子现在是 O(1) 的)。
const c: FbCalCell = {
day: d,
iso: iso,
inMonth: inMonth,
isToday: iso === today,
count: n,
dotColor: color,
lunar: inMonth ? this.lunarOf(iso) : ''
};
return c;
@@ -381,16 +420,11 @@ export struct FbCalendarPage {
for (let i = 0; i < 7; i++) {
const dt: Date = new Date(monday.getFullYear(), monday.getMonth(), monday.getDate() + i);
const iso: string = this.isoOf(dt.getFullYear(), dt.getMonth() + 1, dt.getDate());
const list: FbCalEvent[] = this.eventsOf(iso);
let n: number = 0;
for (const e of list) {
if (e.kind === 'custom' || e.kind === 'anniv') {
n++;
}
}
// ⚠️ v59:这里原来查一遍 events 只为数"有几条"(画小点用)——
// 那排点已按用户要求删除 ⇒ 整段去掉。
out.push({
iso: iso, day: dt.getDate(), w: this.weekLabels[i],
isToday: iso === today, count: n, lunar: this.lunarOf(iso)
isToday: iso === today, lunar: this.lunarOf(iso)
});
}
return out;
@@ -404,8 +438,8 @@ export struct FbCalendarPage {
private agendaDays(): FbAgendaDay[] {
const out: FbAgendaDay[] = [];
const now: Date = new Date();
// 往前 7 天到往后 60 天(够用且不至于太长)
for (let i = -7; i <= 60; i++) {
// ⚠️ 边界必须与 `load()` 请求的 from/to 一致(见 AGENDA_BACK_DAYS 的说明)
for (let i = -AGENDA_BACK_DAYS; i <= AGENDA_FWD_DAYS; i++) {
const dt: Date = new Date(now.getFullYear(), now.getMonth(), now.getDate() + i);
const iso: string = this.isoOf(dt.getFullYear(), dt.getMonth() + 1, dt.getDate());
const list: FbCalEvent[] = this.eventsOf(iso);
@@ -705,7 +739,7 @@ export struct FbCalendarPage {
// (网页版日程页的 `_settings_menu.php` 用的就是那个三横线 SVG。)
FbIconButton({
icon: $r('app.media.fb_ic_menu'),
tint: FbColors.muted,
tint: FbTint.muted,
onTap: (): void => {
this.settingsOpen = true;
}
@@ -865,29 +899,27 @@ export struct FbCalendarPage {
Text(d.w)
.fontSize(FbMetric.fontTiny)
.fontColor(FbColors.faint)
/* 🔴 v59:选中 / 今天的配色与月视图**同一套**(选中深底白字、
今天浅底)—— 用户要的是"点哪天哪天明显",两处不一致会让人
以为换视图就丢了选中。判据同样走方法(见 `isPickedCell` 的说明)。 */
Text(d.day.toString())
.fontSize(FbMetric.fontSmall)
.fontWeight(d.isToday || d.iso === this.pickedIso
.fontWeight(d.iso === this.pickedIso || d.isToday
? FontWeight.Medium : FontWeight.Normal)
.fontColor(d.isToday ? Color.White
: (d.iso === this.pickedIso ? this.accent : FbColors.ink))
.fontColor(d.iso === this.pickedIso
? Color.White
: FbColors.ink)
.width(30)
.height(30)
.textAlign(TextAlign.Center)
.borderRadius(15)
.backgroundColor(d.isToday
.backgroundColor(d.iso === this.pickedIso
? this.accent
: (d.iso === this.pickedIso
? fbPalette(this.accent).brand + '1F' : Color.Transparent))
// 该日条目数(小圆点,最多 3 个)
Row({ space: 2 }) {
if (d.count > 0) {
ForEach(this.dots(d.count), (i: number) => {
FbDot({ dia: 4, fillC: this.accent })
}, (i: number): string => 'wd_' + d.iso + '_' + i.toString())
}
}
.height(5)
: (d.isToday ? fbPalette(this.accent).wash : Color.Transparent))
/* 🔴 v59(用户 2026-10-18):「日期下面有几个点,合格都去掉。」
⇒ 周视图这一排小圆点也删(同一条要求,别只改月视图)。
⚠️ 留等高占位,避免周条高度变化。 */
Row().height(5)
if (d.lunar !== '') {
Text(d.lunar)
.fontSize(8)
@@ -990,42 +1022,50 @@ export struct FbCalendarPage {
@Builder
dayCell(c: FbCalCell) {
Column({ space: 2 }) {
/* 🔴🔴 v59(用户 2026-10-18):「现在点击了某一天之后,并不明显,
应该把这一天的颜色变深,而今天用一个较浅的颜色来显示,
你参考 synccalendar 的做法。」
⇒ 照本机范本 SyncCalendar 的 `weekStrip`:
· **选中** → `selected_bg` **深色实底** + 白字(最强调);
· **今天** → `today_bg` **浅色底** + 常规文字色(弱强调)。
原来的写法正好**反了**:今天用实底(`this.accent` + 白字)抢走了
全部注意力,而选中的那天只有一层 12% 的淡色 —— 点下去几乎看不出变化。
⚠️ 两个都免得同时成立(选中的那天可能也是今天):**选中优先**,
否则"我今天点了今天,却看不到选中效果"。
⚠️ 深浅对照是**这套配色的核心**:选中实底、今天浅底 —— 别再改回去。 */
Text(c.day.toString())
.fontSize(FbMetric.fontSmall)
.fontWeight(c.isToday || c.iso === this.pickedIso ? FontWeight.Medium : FontWeight.Normal)
.fontColor(c.isToday
.fontWeight(this.isPickedCell(c) || c.isToday ? FontWeight.Medium : FontWeight.Normal)
.fontColor(this.isPickedCell(c)
? Color.White
: (c.inMonth ? FbColors.ink : FbColors.faint))
.width(26)
.height(26)
.textAlign(TextAlign.Center)
.borderRadius(13)
.backgroundColor(c.isToday
? this.accent
: (c.iso === this.pickedIso && c.inMonth
? fbPalette(this.accent).brand + '1F'
.backgroundColor(this.isPickedCell(c)
? this.accent // 选中:深色实底
: (c.isToday ? fbPalette(this.accent).wash // 今天:浅色底
: Color.Transparent))
// 农历/节日(有才显示)
if (c.lunar !== '') {
Text(c.lunar)
.fontSize(8)
.fontColor(c.iso === this.pickedIso && c.inMonth ? FbColors.muted : FbColors.faint)
.fontColor(this.isPickedCell(c) && c.inMonth ? FbColors.muted : FbColors.faint)
.maxLines(1)
} else {
// 没有农历就留出等高的空,免得两行格子和一行格子高度不一
Row().height(10)
}
// 事件小点(最多 3 个)
Row({ space: 2 }) {
if (c.count > 0) {
ForEach(this.dots(c.count), (i: number) => {
FbDot({ dia: 4, fillC: c.dotColor })
}, (i: number): string => 'dot_' + c.iso + '_' + i.toString())
}
}
.height(5)
/* 🔴 v59(用户 2026-10-18):「日程,日期下面有几个点,合格都去掉。」
⇒ 那一排小圆点**已删除**(连同 `FbDot` 的引用与 `dots()` 辅助函数
在月视图的调用点)。用户认为它是噪音 —— 日程内容在下面那块
「选中日的日程」里看得更清楚,格子里不需要再暗示"有几条"。
⚠️ 周视图的圆点也一并去掉(同一条要求),别只删月视图。
⚠️ 这里**保留等高占位**:不留的话格子高度会变,7 行月历会整体上移。 */
Row().height(5)
}
.width('100%')
.height(48)
@@ -1041,6 +1081,18 @@ export struct FbCalendarPage {
})
}
/**
* 这一格是不是"当前选中的那天"。
*
* ⚠️ 必须是**方法调用**(build 里读 `this.pickedIso`)而不是把结果当参数传进
* `dayCell` —— 后者按值传递,@Builder 的多值参数不参与观察,
* 选中高亮会**永远停在初始那天**(本项目已在 FbTagChip 上踩过同一个坑,
* 见本文件 `dayPanel()` 上那段说明)。
*/
private isPickedCell(c: FbCalCell): boolean {
return c.inMonth && c.iso === this.pickedIso;
}
/**
* ⚠️ 日程条目行(列表模式)已经搬进 `FbDayEventPanel` 组件里
* (月/周/列表三处共用同一份 + 跟随"显示方式"设置)——
@@ -1057,14 +1109,4 @@ export struct FbCalendarPage {
}
return out;
}
/** 造「点」的索引数组(ForEach 需要数组,不能直接循环次数) */
private dots(count: number): number[] {
const n: number = Math.min(3, count);
const out: number[] = [];
for (let i = 0; i < n; i++) {
out.push(i);
}
return out;
}
}