Files
FamilyBook_harmonyos/entry/src/main/ets/components/Drawer.ets
T
laoyang 853b0e0b20 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 组全过);像素采样确认浮层主题色环实测存在
2026-10-19 12:19:54 +08:00

292 lines
12 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 家书 FamilyBook · 抽屉(鸿蒙端)
*
* 形态对齐 Web 端顶栏 `.nav` 那一排模块入口,但**改成抽屉**(用户建议):
* Web 端导航条最多放 12 个模块,手机宽度下必然溢出;抽屉竖向排列,
* 还能顺带放下「模块说明 + 主题色」这层信息,一眼看清每个模块是干什么的。
*
* 三条形态,同一个组件:
* · **常驻侧栏**(宽屏:窗口短边 ≥ 600vp)—— 固定在左边,占 264vp;
* · **左滑出层**(手机)—— 覆盖内容,点遮罩或选中项即收起;
* · **右滑出层**(手机)—— 同上,只是贴右边(右手单手持机顺手)。
*
* 🔴 抽屉**从哪边出**由「**握姿**」决定(用户 2026-10-16 第 4 条):
* 左手握 → `app.drawerSide = 'left'`;右手握 → `'right'`。
* 逻辑在 `common/FbGrip.ets`(用官方 `multimodalAwareness.motion` 的
* `HoldingHandStatus`),不在本组件里。
* 🔴 **打开方式只有一种:点顶栏左侧的家书图标**(`pages/Index.ets`)。
* ⚠️ **横向滑动开抽屉的旧手势已删**(用户要求)—— 它与页面里的横滑、
* 与"点人名拨号"在 `parallelGesture` 下**并行触发**,冲突无法根治。
*
* ⚠️ 这里**不用 SideBarContainer**:它的控制按钮与「滑出」是内建的一套,
* 样式(图标、位置、宽度动画)很难与家书的视觉统一,且它只能有一层侧栏
* 内容 —— 而我们要在抽屉里放「品牌区 + 模块列表 + 底部账号区」三段。
* 自己用 Stack + 位移动画做,行为完全可控。
*/
import { FbColors, FbIcon, FbMetric, fbTint, FbTint, fbAlpha } from '../common/Theme';
import { FbAppState, fbAppState } from '../common/Store';
import { FbModuleDef, FB_MODULES, fbDefaultNavModules, fbThemeVars } from '../model/Modules';
import { FB_DEFAULT_SLOGAN } from '../model/DemoData';
import { FbAvatar } from './Widgets';
/**
* 底部手势条区域的留白(vp)。
*
* 🔴 应用已开 `setWindowLayoutFullScreen(true)`(见 EntryAbility):
* 内容会铺到屏幕最下沿,而**系统手势条已隐藏**(用户要求"下面没有任何的条")
* ⇒ 手势区**没有任何视觉提示**,贴着底边的元素很容易被"从下往上滑"误触。
* 这里留一点缓冲,是给"手势区"而不是给"某个控件"。
*/
const FB_BOTTOM_GAP: number = 16;
@ComponentV2
export struct FbDrawer {
/** 全局状态(父组件传入,保证与页面同一份实例) */
@Param app: FbAppState = fbAppState();
/** 抽屉宽度 */
@Param drawerW: number = FbMetric.drawerW;
/** 是否以「覆盖层」形态出现(宽屏时为 false = 常驻) */
@Param overlayMode: boolean = true;
/** 覆盖层形态下:点遮罩关闭 */
@Event onDismiss: () => void = () => {
};
/** 抽屉里列出的模块(接口接上后由 user_settings('nav_modules') 下发) */
@Local items: FbModuleDef[] = fbDefaultNavModules();
/**
* 品牌区:书名(家书)+ 标语。
*
* 🔴🔴 v59(用户 2026-10-18):副标题从原来的**家庭名**(「我的家」/「杨家」)
* 换成**标语**。用户原话:
* 「进到系统里面之后,打开抽屉,左上角有家书的名字,上面写了"我的家"。
* 这个地方也换成我们的标语。」
* 「如果这个用户自己有标语,他可以写一句自己的标语;如果他有自己的标语,
* 这个地方就是用户自己自定义的标语。如果他没有标语,就用我们给他的这个标语。」
* ⇒ 三层优先级:**个人 slogan → 家庭 motto → 应用默认口号**。
* 这个拼接**不在端上做** —— 服务端 `Family::slogan()` 已经算好并放在
* `session.slogan` 里(网页端首页是同一个函数,两边永远一致)。
* 端上只在"还没拿到"时回落 `FB_DEFAULT_SLOGAN`。
* ⚠️ 别再改回 `familyTitle` —— 那个字段现在只用于别处(如"某某的家"这类文案),
* 而且用户明确说这一格要放标语。
*/
@Builder
brandHeader() {
Row({ space: 10 }) {
Image($r('app.media.fb_logo'))
.width(32)
.height(32)
.borderRadius(9)
Column({ space: 1 }) {
Text('家书')
.fontSize(16)
.fontWeight(FontWeight.Medium)
.fontColor(FbColors.ink)
.letterSpacing(1)
// 标语:用户自己的 → 家庭的 → 应用默认(服务端已算好,见上面说明)
Text(this.sloganText())
.fontSize(FbMetric.fontTiny)
.fontColor(FbColors.muted)
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%')
// 顶部内边距 = 状态栏高度 + 一点呼吸,保证不压状态栏
.padding({ left: 18, right: 12, top: 16 + this.app.statusBarH, bottom: 10 })
}
/** 该显示的标语(会话里没有就回落应用默认口号,**永远不空**) */
private sloganText(): string {
const s: string = this.app.session.slogan;
return s !== '' ? s : FB_DEFAULT_SLOGAN;
}
/** 一个模块行 */
@Builder
moduleRow(m: FbModuleDef) {
Row({ space: 12 }) {
// 图标底:选中时用模块色实底(白图标),未选中时用极浅染色底(彩色图标)
Stack() {
Image(FbIcon.module(m.key))
.width(18)
.height(18)
.renderMode(ImageRenderMode.Template)
.colorFilter(fbTint(this.isActive(m.key) ? FbTint.white : fbThemeVars(m.color).brand))
}
.width(34)
.height(34)
.borderRadius(10)
.backgroundColor(this.isActive(m.key) ? fbThemeVars(m.color).brand : this.washOf(m.color))
Column({ space: 1 }) {
Text(m.name)
.fontSize(FbMetric.fontBody)
.fontWeight(this.isActive(m.key) ? FontWeight.Medium : FontWeight.Normal)
.fontColor(FbColors.ink)
Text(m.desc)
.fontSize(FbMetric.fontTiny)
.fontColor(FbColors.faint)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%')
.padding({ left: 12, right: 12, top: 9, bottom: 9 })
.borderRadius(FbMetric.radiusS)
.backgroundColor(this.isActive(m.key) ? this.washOf(m.color) : Color.Transparent)
.onClick(() => {
this.app.switchModule(m.key);
this.onDismiss();
})
}
/** 当前是否选中该模块 */
private isActive(key: string): boolean {
return this.app.activeModule === key;
}
/** 模块色的极浅染色底(选中行 / 图标底) */
private washOf(hex: string): string {
// 与 Theme.fbPalette 的 wash 同口径:压饱和、抬明度到 0.94
const t = fbThemeVars(hex);
return fbAlpha(t.brand, 0.10);
}
build() {
Column() {
this.brandHeader()
// 模块列表:可滚动,模块多时不会把底部账号区挤掉
List({ space: 1 }) {
ForEach(this.items, (m: FbModuleDef) => {
ListItem() {
this.moduleRow(m)
}
}, (m: FbModuleDef): string => 'nav_' + m.key)
}
.layoutWeight(1)
.width('100%')
.padding({ left: 6, right: 6 })
.scrollBar(BarState.Off)
.edgeEffect(EdgeEffect.Spring)
/* 🔴 v61(用户 2026-10-19):「左侧抽屉,把通知消息、个人设置、
修改密码、系统设置,都去掉,放在个人设置页,就是在点击右侧头像那里。
消息通知不用放进去,直接在头像旁边那里就可以了。」
⇒ 原来这里的底部账号区(四行:通知消息 / 个人设置 /
修改密码 / 系统设置)**整块已删**:
· 通知 → 顶栏铃铛(一直在头像左边,比藏抽屉里近得多);
· 个人设置 / 修改密码 / 系统设置 → 点右上角头像进「个人设置」页。
⚠️ 抽屉现在是**纯模块导航**:品牌区 + 模块列表。
⚠️ 底部仍留手势条留白 —— 全屏布局下抽屉铺到屏幕最下沿,
最后一行的点击区贴着边缘容易被"从下往上滑"误触。 */
Column()
.width('100%')
.height(FB_BOTTOM_GAP)
}
.width(this.drawerW)
.height('100%')
.backgroundColor(FbColors.surface)
// ⚠️ 侧栏右侧的分隔线只在常驻形态下需要;覆盖层形态下它贴着屏幕边,加了反而怪
.border(this.overlayMode
? { width: 0 }
: { width: { right: 1 }, color: FbColors.line2 })
}
}
/**
* 抽屉 + 内容 的整体骨架。
*
* 两种形态在这里分派:
* · 宽屏 → Row(抽屉占位 + 内容区);
* · 窄屏 → Stack(内容铺满,抽屉带遮罩浮在上面)。
*/
@ComponentV2
export struct FbDrawerShell {
@Param app: FbAppState = fbAppState();
/** 内容区(由父组件通过尾随 lambda 传入) */
@BuilderParam content: () => void;
build() {
// 抽屉贴哪边由 drawerSide 决定 ⇒ Stack 的 alignment 也跟着换。
Stack({
alignContent: this.app.drawerSide === 'right' ? Alignment.TopEnd : Alignment.TopStart
}) {
// ---- 宽屏:并排 ----
if (this.app.wideLayout) {
Row() {
FbDrawer({
app: this.app,
overlayMode: false
})
Column() {
this.content()
}
.layoutWeight(1)
.height('100%')
}
.width('100%')
.height('100%')
} else {
// ---- 窄屏:内容铺满 + 抽屉浮层(可从左或右滑出)----
Column() {
this.content()
}
.width('100%')
.height('100%')
if (this.app.drawerOpen) {
// 遮罩
Column()
.width('100%')
.height('100%')
.backgroundColor('#66000000')
.onClick(() => {
this.app.drawerOpen = false;
})
.transition(TransitionEffect.OPACITY.animation({ duration: 180 }))
// 抽屉本体:由 drawerSide 决定贴左还是贴右
// ⚠️ 用 Stack 的 alignment 而不是 margin 来定位:
// 动画是 translateX,用 alignment 铺位更稳(不参与测量)。
FbDrawer({
app: this.app,
overlayMode: true,
onDismiss: (): void => {
this.app.drawerOpen = false;
}
})
.transition(TransitionEffect.translate({
x: this.app.drawerSide === 'right' ? FbMetric.drawerW : -FbMetric.drawerW
}).animation({ duration: 220, curve: Curve.FastOutSlowIn }))
}
}
}
.width('100%')
.height('100%')
.backgroundColor(FbColors.paper)
// 🔴🔴 **横向滑动开抽屉的手势已删除**(用户 2026-10-16 第 3、4 条)。
//
// 用户原话:「这个列表上左滑、右滑和点击后打电话的事件冲突了……
// 如果实在不行,就把左滑、右滑弹出抽屉这个功能去掉吧,
// 只保留点击标题或图标后弹出抽屉的功能。」
// 并再次强调:「**先这样做,把滑动出抽屉这个功能先去掉**,
// 因为它现在在很多地方都跟别的功能有冲突。」
//
// 历史:`parallelGesture(PanGesture)` + `fbHScrollBusy()` 的那套"让位"
// 机制(见 git 历史)**治不了根本** —— 因为 `parallelGesture` 是**并行**识别,
// 它和应用自己的横滑/点按/拨号**同时**成立,没有"谁先抢到"的机制。
// 名册里"滑动既弹抽屉又跳拨号"就是这个并行识别的必然结果。
// ⇒ 唯一可靠的办法是**不用横滑手势**。
//
// 抽屉入口保留在:**顶栏左侧的家书图标**(`pages/Index.ets` 的 `topBarSlot`)。
// 抽屉从哪边出,改由「**握姿**」决定(见 `FbGripWatcher` / `app.drawerSide`):
// 左手握 → 从左边出;右手握 → 从右边出。
}
}