v61:顶栏标题=模块名 / 抽屉瘦身 + 头像进个人设置 / 删除按钮红底 / 浮层动画 / 日程卡片与手势

用户这一批九条,逐条对应:

1)「页面上左上角的标题,不再是家书了,而是切换到那个模块,就显示哪个模块的标题。
   这样在页面内容部,就不再出现模块的名字标题了。」
   → TopBar 主标题改读 moduleName()(通知/设置/个人设置三个非模块路由也有名字);
     观影/纪念日/读书/笔记/通知五处页内大标题删掉。

2)「按钮,也需要和主题色保持一致,出现文字的时候,例如观影模块,
   在左上角出现的时候,颜色也需要和主题色保持一致。」
   → 标题色走 fbModuleColor(app.activeModule)(一处算,11 个页面不用各写)。

3)「弹框的外圈颜色,跟模块的主题色保持一致。」
   → 日程详情原来传 bookColor(日历本色)⇒ 改 accentColor();
     7 个模块设置浮层补传 accent: this.accent。

4)「删除按钮全部用红色背景。」
   → 日程编辑/人员编辑从红描边改红底白字;详情「删除联系人」从浅红底改红底白字;
     两处确认框统一 FbColors.red。

5)「做几个动画,例如,点击名册列表,应该有一个过渡动画出现弹框,
   点击详情,框变大了,也应该有一个过渡动画,点编辑,也有动画。」
   → 新建 common/FbAnim.ets(fbMaskTransition / fbSheetTransition /
     fbSheetGrowTransition / fbViewTransition),9 个页面共 17 遮罩 + 16 浮层挂上;
     简要卡→详情用 0.96→1 的"继续长大"(0.94 会先缩一下)。

6)「左侧抽屉,把通知消息、个人设置、修改密码、系统设置,都去掉,
   放在个人设置页,就是在点击右侧头像那里。消息通知不用放进去,
   直接在头像旁边那里就可以了。」
   → 抽屉底部那四行整块删除(抽屉只剩品牌区 + 模块列表);
     新建 pages/account/FbAccountPage.ets(整页,__account 分发),
     点头像 switchModule('__account');改密码真打服务端新接口。

7)「日程……日期下面:12本日历可见,本月124条,这句话去掉。
   点击日程弹出的框,编辑按钮也颜色也要修改。
   全天日程,例如农历,上面显示无标题,读不到这条日程,这个有问题。」
   → 删统计行;编辑按钮从写死名册青 #4A7C82 改模块主题色;
     真根因:衍生条目(农历/节气)的 detailFull **一直是 null**
     (它们没有 /events/<id> 可拉,原来直接 return)⇒ 垫 shellOf(),
     并把"整卡报错"改成"照常画内容 + 一行小字";
     空标题兜底从「(无标题)」改成日历本名。

8)「再做几个卡片,先做日程的,这个参考 synccalendar。」
   → FbDayEventPanel.eventRow 照 SyncCalendar:左侧 4vp 圆头色条 + 标题 +
     时间/全天 + 「↻ 重复」徽标 + 右端日历本名(限宽 30%、同色);
     一行一张独立圆角卡(12)+ 描边,卡间 space 8,不再用 Divider 连成一片。

9)「日程,有很多手势,你参考 synccalendar。」
   → 月历上滑→周视图、周条下划→月视图、周条左右滑→换周。
     真机实测两次踩空后定下写法:**竖向必须 parallelGesture(PanGesture)**
     (外层是 Scroll,SwipeGesture 的"快速划动"会被它的内置滚动先吃掉,
      gesture/priorityGesture 都不行);**横滑用 gesture(PanGesture)**
     (同一节点两条方向不同的 parallelGesture 会互相压制);
     方向按累计位移判、并要求 |dy| > |dx|。切视图走 animateTo + fbViewTransition。

验证:
· 编译 BUILD SUCCESSFUL
· ArkTS 守卫 493/0;Schema 列守卫 12/0;反向验证 39/0(新增 22 条注入逐条确认能变红)
· 服务端接口 e2e 20/0(独立副本 + 真 MariaDB,5 种错误路径 + 原令牌仍有效)
· 真机 _fb_v61_verify.py 21/0(6 组全过);像素采样确认浮层主题色环实测存在
This commit is contained in:
2026-10-19 12:19:54 +08:00
parent 1a67247407
commit 853b0e0b20
22 changed files with 1252 additions and 362 deletions
+40 -155
View File
@@ -2,7 +2,7 @@
* 家书 FamilyBook · 顶栏(鸿蒙端)
*
* 用户 2026-10-12 定的形态:
* · **应用图标 + 「家书」标题**贴左上(下行小字显示当前模块);
* · **应用图标 + 当前模块名**贴左上(用户 2026-10-19 改:不再是固定的「家书」);
* · **点图标 = 开抽屉**(用户要求:汉堡按钮去掉后,把这个功能挪到图标上);
* · 顶栏底色**白色**(与下面的个人信息卡同色,连成一片);
* · 顶栏**并入页面滚动流**,向上滑动时跟着内容一起滑走。
@@ -19,12 +19,10 @@
import { FbColors, FbIcon, FbMetric, FbTint, fbTint } from '../common/Theme';
import { FbAppState, fbAppState } from '../common/Store';
import { FbSyncState, SYNC_DONE, SYNC_FAIL, SYNC_RUNNING, fbSyncState } from '../common/FbSyncStore';
import { fbModuleOf } from '../model/Modules';
import { fbModuleColor, fbModuleOf } from '../model/Modules';
import { FbAvatar } from './Widgets';
import { FbPersonSheet } from './FbPersonSheet';
// ⚠️ `inst()` 拿单例(本工程所有 repo 都是这个约定,见 FbPersonRepo 文件头);
// `FB_TOKEN_STORE` 在 **FbTokenStore.ets** 里(不是在 FbDeviceStore)。
import { FbPersonRepo, FbRosterPerson } from '../data/FbPersonRepo';
import { FB_TOKEN_STORE } from '../data/FbTokenStore';
import { fbMediaUrl } from '../data/FbHttp';
import { syncAll } from '../data/FbSyncAll';
@@ -48,14 +46,6 @@ export struct FbTopBar {
/** 同步面板是否打开 */
@Local syncSheet: boolean = false;
/** 「我的详情」浮层是否打开(点头像触发) */
@Local myDetailSheet: boolean = false;
/** 「我的详情」里的人(null = 还没取到) */
@Local myDetail: FbRosterPerson | null = null;
/** 「我的详情」是否在加载 */
@Local myDetailLoading: boolean = false;
/** 「我的详情」的兜底提示(拿不到资料时显示) */
@Local myDetailNote: string = '';
build() {
// 🔴 顶栏横向镜像 —— 「质感握姿」(用户 2026-10-16 第 4 条):
@@ -87,15 +77,17 @@ export struct FbTopBar {
})
Column({ space: 0 }) {
Text('家书')
// 🔴 标题 = **当前模块名**(用户 2026-10-19:「不再是家书了,
// 而是切换到那个模块,就显示哪个模块的标题」)。
// ⇒ 模块名在页面内容部**不再重复出现**(各页头的大标题已删)。
// ⚠️ 文字色用**模块主题色**(同一条要求:「出现文字的时候,
// 例如观影模块,在左上角出现的时候,颜色也需要和主题色保持一致」)。
// ⇒ 走 `fbModuleColor()` 而不是各页自己传色:顶栏只有一个,
// 在这里算一次全模块都一致(也避免了 11 个页面各写一遍)。
Text(this.moduleName())
.fontSize(19)
.fontWeight(FontWeight.Bold)
.fontColor(FbColors.ink)
.maxLines(1)
// 副标题显示当前模块名 —— "品牌 + 当前位置"一行说清
Text(this.moduleName())
.fontSize(FbMetric.fontTiny)
.fontColor(FbColors.muted)
.fontColor(this.moduleColor())
.maxLines(1)
}
.alignItems(HorizontalAlign.Start)
@@ -244,28 +236,13 @@ export struct FbTopBar {
.justifyContent(FlexAlign.Center)
.margin({ left: FbMetric.gapTopActions })
.onClick((): void => {
this.openMyDetail();
})
// 「我的详情」浮层 —— 复用名册那个人详情组件(同一个组件、同一套字段)
//
// ⚠️ 用 `bindSheet` 而不是自己在 `build` 里叠一层遮罩:
// 这条顶栏在**每个模块页**都会渲染(`topBarSlot`),
// 自己叠遮罩要处理"盖住悬浮栏 / 盖住抽屉 / 手势返回"一堆事;
// bindSheet 是系统能力,这些它都管(同步面板也是这么做的)。
//
// ⚠️ `onDisappear` 必须复位 `myDetailSheet`(点遮罩/下滑关闭都会走它),
// 否则下次点头像**打不开**(状态位还是 true 但面板已经不在)。
.bindSheet(this.myDetailSheet, this.myDetailPanel(), {
// ⚠️ 用 `LARGE`(= 大高度、内容自适应)而不是 `FIT_CONTENT`:
// 详情卡片有 `maxHeight: '72%'` 的内部约束,FIT_CONTENT 下
// 系统会试图把 sheet 压到"刚好包住内容",与那个百分比约束打架。
height: SheetSize.LARGE,
dragBar: true,
showClose: false,
backgroundColor: FbColors.surface,
onDisappear: (): void => {
this.myDetailSheet = false;
}
/* 🔴 v61(用户 2026-10-19):「左侧抽屉,把通知消息、个人设置、
修改密码、系统设置,都去掉,放在个人设置页,就是在点击右侧头像那里。」
⇒ 点头像**进个人设置页**(整页),不再弹「我的详情」浮层。
⚠️ 换的是 `switchModule` 而不是自己开浮层:整页才有地方放
"改密码 / 系统设置 / 退出登录",而那三样都是页面级的。
⚠️ 顶栏在**每个模块页**都存在 ⇒ 这里只切状态位,渲染由 Index 分发。 */
this.app.switchModule('__account');
})
}
.width('100%')
@@ -300,11 +277,32 @@ export struct FbTopBar {
.border({ width: { bottom: 1 }, color: FbColors.line2 })
}
/**
* 顶栏标题 = 当前模块名。
*
* 🔴 两个非模块路由(通知 / 设置)也要有名字 —— 它们不在 `FB_MODULES` 里,
* 不认就会回落「家书」,而那一页明明不是首页。
*/
private moduleName(): string {
const m = fbModuleOf(this.app.activeModule);
const k: string = this.app.activeModule;
if (k === '__notifications') {
return '通知消息';
}
if (k === '__settings') {
return '设置';
}
if (k === '__account') {
return '个人设置';
}
const m = fbModuleOf(k);
return m !== undefined ? m.name : '家书';
}
/** 顶栏标题色 = 当前模块的主题色(认不出回落到品牌砖红) */
private moduleColor(): string {
return fbModuleColor(this.app.activeModule);
}
/**
* 手动同步(面板里的按钮)。
*
@@ -333,119 +331,6 @@ export struct FbTopBar {
: '';
}
/**
* 打开「我」的详情浮层(点右上角头像触发)。
*
* 🔴 v59b(用户 2026-10-18):「点击头像进入我的详情页面。」
*
* 为什么**先打开浮层、再补数据**(而不是等网络回来才开):
* 本地优先是本应用的立身之本 —— 点一下要**立刻**有东西出来,
* 断网时也必须能看。本地库那份已经够画详情的前几段,
* 网络回来后再原地补全(`fetchDetail`)。
*
* ⚠️ 失败时**不弹错误**:`me.id` 拿不到(未登录/数据还没同步下来)就只弹一句
* "资料还没同步下来",而不是让用户对着一个空白浮层猜。
*/
private async openMyDetail(): Promise<void> {
const meId: number = this.app.session.me.id;
if (meId <= 0) {
this.myDetailNote = '还没拿到你的资料,请先同步一次';
this.myDetailSheet = true;
return;
}
/* ⚠️ 先开浮层(`myDetailSheet = true`)再 await:
否则网络慢时用户点了一下**几十毫秒内毫无反应**,会以为按钮坏了。
`loading` 由浮层自己显示,观感上"点开就有东西"。 */
this.myDetailLoading = true;
this.myDetail = null;
this.myDetailSheet = true;
try {
const local: FbRosterPerson | null = await FbPersonRepo.inst().byId(meId);
this.myDetail = local;
this.myDetailLoading = false;
// 本地有 ⇒ 顺手用网络那份补全(口径与名册里点某人完全一致)
const token: string = FB_TOKEN_STORE.get();
if (token !== '' && local !== null) {
const full: FbRosterPerson | null = await FbPersonRepo.inst().fetchDetail(meId, token);
// ⚠️ 浮层可能已经被关掉、或期间换了人 ⇒ 只在"还开着同一个人"时才覆盖
if (full !== null && this.myDetailSheet
&& this.myDetail !== null && this.myDetail.id === meId) {
this.myDetail = full;
}
}
} catch (err) {
const e: BusinessError = err as BusinessError;
hilog.warn(0x0000, 'FamilyBook', '取我的详情失败:%{public}s',
e.message !== undefined ? e.message : JSON.stringify(err));
this.myDetailLoading = false;
}
}
/**
* 「我的详情」面板(点头像弹出)。
*
* ⚠️ 三种状态都要有话说(用户 2026-10-18 报过"点了没反应"这类问题):
* · 加载中 ⇒ 「正在读取…」(不要让浮层空着)
* · 拿到了 ⇒ 复用 `FbPersonSheet`(姓名/称呼/电话/生日/单位/标签…)
* · 拿不到 ⇒ 显式一句 `myDetailNote`(而不是空白)
*/
@Builder
myDetailPanel() {
Column() {
if (this.myDetailLoading) {
Row({ space: 8 }) {
LoadingProgress().width(18).height(18).color(FbColors.brand)
Text('正在读取我的资料…')
.fontSize(FbMetric.fontSmall)
.fontColor(FbColors.muted)
}
.width('100%')
.justifyContent(FlexAlign.Center)
.padding({ top: 28, bottom: 28 })
} else if (this.myDetail !== null) {
FbPersonSheet({
person: this.myDetail,
/* 🔴🔴 必须传 embedded(2026-10-18 真机踩出来的真 bug):
`FbPersonSheet` 的根节点是"铺满全屏 + 内部居中"的浮层形态
(它的设计宿主是"页面里叠遮罩 + Stack 居中",名册页就这么用)。
直接塞进 `bindSheet`(内容要自适应高度)会**测量互相打架**:
真机症状是**每次点击 sheet 都创建了、紧接着
`AceDrag: Drag gesture has been canceled` —— 起来立刻被关**,
界面上看就是"点了没反应"(极易误判成"点击没接上")。 */
embedded: true,
/* 🔴 不给「删除」:这是"我自己的详情"。
删除自己既没意义又危险(服务端是物理删除,删完账号都进不去)。
名册里的详情卡才显示删除(那里 `canDelete` 取默认值 true)。 */
canDelete: false,
onClose: (): void => {
this.myDetailSheet = false;
},
// ⚠️ 这里**不给编辑入口**:编辑要跳名册页那套编辑浮层(含标签/电话增删),
// 而顶栏在**每个模块页**都存在 —— 在这里接编辑会变成
// "在日程页里编辑家庭成员的资料",职责与页面上下文都不对。
// 要看/改详细资料,从名册进(那是它的主场)。
onEdit: (): void => {
this.myDetailSheet = false;
}
})
} else {
Text(this.myDetailNote !== ''
? this.myDetailNote
: '暂时拿不到你的资料,下拉同步一次再试')
.fontSize(FbMetric.fontSmall)
.fontColor(FbColors.muted)
.textAlign(TextAlign.Center)
.width('100%')
.padding({ top: 28, bottom: 28, left: 24, right: 24 })
}
}
.width('100%')
/* ⚠️ 不要给这层加 `constraintSize({maxHeight})`:
sheet 的高度由 `SheetSize.LARGE` 决定,卡片自己已有 `maxHeight:72%`;
再叠一层百分比约束只会让"高度该多高"变成三方打架
(真机症状就是浮层起来立刻被关)。 */
}
/**
* 同步面板(点顶栏图标弹出)。
*