## 起因 用户 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 个文件已同步(含软著全称改为「岁月家家庭生活档案管理软件」) - 官网已部署,线上标题已是新名字 - 软著申请尚未提交 —— 正好一次写对新名字,不需要变更
292 lines
12 KiB
Plaintext
292 lines
12 KiB
Plaintext
/**
|
||
* 家书 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`):
|
||
// 左手握 → 从左边出;右手握 → 从右边出。
|
||
}
|
||
} |