Files
FamilyBook_harmonyos/entry/src/main/ets/components/Drawer.ets
T
laoyang 825008657b 品牌改名:家书 → 岁月家 (laoyang/FamilyBook#38)
## 起因

用户 2026-11-01:「家书已经有人做了,八月份刚上线的」。

核实:「家书 HomeBook」(广州家书科技有限责任公司,粤ICP备2026096420号,
2026-06 成立、08 上线)。功能**部分重合**(家庭记录 + 相册 + 家谱)。
华为审核指南 1.3「应用名称不得和其他应用名称相同,避免混淆」,
虽然个人与企业主体查重不互通、技术上能过,但被人工驳回概率高。

## 定名过程(用户逐个否掉)

| 候选 | 否决原因 |
|---|---|
| 寸草 | — |
| 家卷 | ❌ 与「家眷」同音,口头传播会打错字 |
| 家时光 | ❌ App Store 已有「家·时光」定位几乎一致;且「家+时光」是常见组合,辨识度低 |
| 家纪 | ❌ 已有重名 |
| **岁月家** | ✅ 搜不到同名应用;是口号的直接展开(岁月 + 家) |

## 最终定稿(用户明确指定)

```
应用名      岁月家
主标语      一家人用岁月写成的一本书     ← 不变
一句话简介  一家人用岁月写成的一本书     ← 改为与标语相同
官网标题    岁月家 · 一家人用岁月写成的一本书
英文名      FamilyBook                ← 不变
包名        familybook.yangyq.net     ← 不变
```

## 改动范围(17 + 18 处,12 个文件)

### 应用名(17 处)
`AppScope/resources/base/element/string.json` 的 app_name,
`entry/.../string.json` 的 16 条(应用名、卡片名、无障碍描述)。

### 用户可见文案(18 处)
`FbBackgroundSync` / `EntryAbility`「正在同步家书数据」、
`FbAttachOpen`「家书附件」相册名、`FbHttp` 地址校验失败提示、
`FbCardData` / `FbCardMediaData` 卡片空态「先在「家书」里填写服务器地址」、
`TopBar` / `Drawer` / `FbLoginPage` 的应用名文本、`FbSettingsPage` 的说明。

## 刻意不改

- **包名 / bundleName**(用户明确要求不变,且改了等于换一个应用)
- **英文名 FamilyBook**、目录名、仓库名(改了会让 git 历史、部署脚本、证书路径全跟改)
- **代码注释**里 200+ 处「家书 FamilyBook · 某模块」—— 不影响运行时,
  改了只会制造噪音 diff,让将来 blame 变难
- **主标语**里一个字都没动

## 验证

### 编译
`BUILD SUCCESSFUL`,33 tasks。

### 产物实测
解包 `.hap` 的 `resources.index`:
- 「岁月家」出现 **17** 次
- 「家书」出现 **0** 次

### 全局一致性校验(31 项全过)
`_tooling/check_rename.js`:
- 客户端 app_name / EntryAbility_label / 产物内容
- .ets 用户可见文案无残留(排除注释)
- 服务端非注释代码无残留
- `Auth::homeTitle()` 用「某某某的家庭岁月」
- `DEFAULT_SLOGAN` 未被改动
- 官网线上 5 个页面无「家书」、标题含岁月家、渲染后 h1 是品牌名
- 材料 app_name / 一句话简介 / 包名 / versionCode / 域名

⚠️ 校验过程中我自己的判据出了 2 次假红:
① 注释识别不全(漏了 `/**` 单行块注释和 `<!--` HTML 注释)→ 补齐 isCommentLine
② 「为什么叫岁月家」整节重写后,旧断言还在查「书信」「家与书」
   (那是**旧名字**的逻辑)→ 改成查「岁月」+「家」的拆解

## 一处语义决策(问过用户)

网页端 `Auth::homeTitle()` 生成「某某某的家书」—— 这是**产品核心语义**
(每个家有自己的那一本),直接换成品牌名会读成「某某某的岁月家」,不通。

用户定:改成 **「某某某的家庭岁月」**。
兜底(无姓名时)仍用品牌名「岁月家」。

## 连带影响(不阻塞)

- 上架材料 7 个文件已同步(含软著全称改为「岁月家家庭生活档案管理软件」)
- 官网已部署,线上标题已是新名字
- 软著申请尚未提交 —— 正好一次写对新名字,不需要变更
2026-11-01 19:55:36 +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`):
// 左手握 → 从左边出;右手握 → 从右边出。
}
}