Files
FamilyBook_harmonyos/entry/src/main/ets/components/FbModuleSettingsSheet.ets
T
laoyang 4a063114c5 v62:修 P0 登录被踢 / 关闭按钮统一右上角 / 观影三处 / 影院管理补齐 / 名册三处
用户 2026-10-19 第二批八条(原话逐条对应):

═══ ⑨ P0:「现在还是不稳定,会退出,提示:这台设备已经被退出登录,请重新登录。」

真因**是我 v61 自己埋的**:「个人设置」页(`FbAccountPage`)里两处 `fbApi`
调用**取了 `token` 变量却没传进请求选项** ⇒ 没有 Authorization 头 ⇒ 服务端 401
⇒ `FbHttp` 的全局回调 `unauthorizedCb` **清掉本地令牌 + 踢回登录页**。
而本页在 `aboutToAppear` 里就调它 ⇒ **点一下右上角头像,App 就退出登录**。

⚠️ 我上一轮把同一现象归因成"残留旧令牌 / 设备会话被顶掉"(还去线上查了
   `api_tokens` 表,一切正常)—— **那个结论是错的**,等于把 bug 归给了环境。

修法(两处调用 + **两道架构防线**,缺一不可):
  ① 修两处调用,把 token 传进去;
  ② `fbApi` 内部:**本地有令牌、调用方又没传时自动补上**
     (大部分接口本来就要鉴权,补上是"纠正";`/login`、`/ping` 带了也无害);
  ③ **401 只在"确实带了令牌"时才算被踢** —— 否则"没带令牌"也会触发全局清令牌,
     那正是这次事故的机制。

═══ ④「你这个按钮到底是在弹框的右上角关闭,还是在下面关闭……全放在右上角。」

选**右上角 ✕**(底部已被 编辑/保存/删除 占满,再塞关闭会堆三四个按钮,分不清主操作)。
新增 `components/FbSheetClose.ets`:
  · `FbSheetClose`  —— 32vp 圆形 ✕(热区 40+,小图标在手机上点不中);
  · `FbSheetTitleBar` —— 左标题 + 右收尾(设置类给 `confirmText` 显示「完成」)。
9 个浮层全接;名册详情 / 日程详情 / 简要卡**底部不再有「关闭」**。
⚠️ 浅色底走 `fbAlpha`,**不能拼 `+ '14'`** —— ArkUI 八位色是 `#AARRGGBB`
   (本项目曾因写 `#00000066` 导致遮罩**从来没显示过**)。

═══ ①「观影……弹出的框左上角有一条灰色的线,不知道是什么。」

那是浮层顶部的**抽屉把手**(36×4 的 `line2` 浅灰短线)。本意是"可下拉关掉"的提示,
但 ① 它落在左上角、旁边没有文字;② 这个浮层的关闭本来就不靠下拉。
⇒ 用户认不出它 = 它没起作用,**删掉**(不改成居中 —— 那只是把灰线挪个位置)。

═══ ②「然后这个观影人的头像并没有带进来。」

同看人的 `FbAvatar` **漏传 `imageUrl`** —— 服务端 `Movie::attendees()` 本来就
`SELECT p.avatar`、端上 `parseAttendees` 也解析进了 `a.avatar`,
纯粹是**渲染时忘了用它** ⇒ 有头像的人也退化成色块。
⚠️ 与名册详情 2026-10-15 那个是同一个 bug(当时是 `FbPersonSheet` 漏传)。

═══ ③「点击这个图片呢,不能左右滑动看所有的这个图片。」

全屏查看器原来**只有底部「上一张/下一张」文字按钮**。现在整张大图左右滑翻页。
⚠️ 用 `.gesture(PanGesture(Horizontal))` 而不是 `SwipeGesture`:
   `SwipeGesture` 要"快速划动"才识别,在有滚动容器的页面容易被先吃掉
   (日程页实测:`gesture`/`priorityGesture` 都不触发)。`PanGesture` 看**累计位移**。
⚠️ 判定要求 `|dx| >= 40 && |dx| > |dy|`,否则竖划会被误判成翻页。

═══ ⑤「影院管理……增加影院呢、修改影院呢、高级搜索、统计、年度报告,这些全部都应该补充进去。」

端上原来**只有一个只读列表**(连"新增"都没有)。补成四个页签:
  · **影院**:新增 / 修改 / 删除(服务端 CRUD 早就有,端上一直没接)+ 删除二次确认;
  · **统计**:总览(看过/影院/人数/人次)+ 按影院/按年/按月的条形图;
  · **搜索**:片名 / 影院 / 起止日期(条件全空时不查,与网页 `hasCond` 同行为);
  · **年报**:按年切换(第一场/最后一场、最常去的影院、一起看最多的人、每月分布)。
⚠️ **统计/年报必须联网**(`/movies/stats|report`):口径要与网页**完全一致**
   (同一份 `MovieStats`)。本地库算出来的是"已同步的那部分",
   少同步一次就少一部 ⇒ 用户会发现"网页 124 部、App 121 部"。
   ⇒ 断网时明说"需要联网",而不是给一个偏低的数字。
⚠️ 影院写操作后**立刻回写本地库**(`refreshCinemas`),否则刚建的那家要等下次同步
   才出现,看着像失败;并用 `INSERT OR REPLACE` 而非"清空再插"
   (中途失败会留下一个空表 = 用户的影院列表整个消失)。

═══ ⑥「名册搜索,搜索后,下面有一个白色的框,什么内容都没有,删除。」

那是列表**末尾给悬浮栏留高度的占位块**(`height(FB_DOCK_RESERVE)`),
底色是 `FbColors.surface`(**白**)而列表容器是 `paper`(**米**)——
列表内容一短(搜索只剩一两条)它就露出来,看着像"什么都没有的白框"。
⇒ 改底色与列表同色。**不能删**:删了最后一张卡片会被悬浮栏压住。

═══ ⑦「如果一个人没头像,在列表是有一个文字的头像的,那么点击后弹框,也应该有。」

`FbAvatar` 原来是 `if/else`:有 `imageUrl` 就**只画 Image**,
而图片**加载失败时 `Image` 什么都不显示**(只露出纯色底)⇒ 看着是个光板。
(`alt()` 只在**加载中**起作用,失败后是空白。)
⇒ 改成**叠加**:底色 + 文字头像**常驻**,图片有就叠在上面
(加载成功即盖住文字,失败则自动露出文字头像)。
⚠️ 之前注释说"图层叠会错位"——那是因为当时想靠 Image 的 intrinsic size 撑尺寸;
   现在两层都显式给 `dia`,尺寸来源唯一,不存在错位。

═══ ⑧「现在名册的列表太空了,应该显示更多内容,例如工作单位、职务。」

列表卡片加一行「单位|职务」(独立成行,避免与标签/生日挤)。
⚠️ 用 `persons.org` / `persons.title` 而**不是** `person_jobs`:
   列表读的是**本地库**,而本地库只存 `persons` 那一行
   (`person_jobs` 是一对多,端上没同步那张表)⇒ 服务端加了字段也到不了列表。
   线上实测 801 人里 192 人有单位、48 人有职务,列表立刻有内容。

验证:
· 编译 BUILD SUCCESSFUL
· ArkTS 守卫 536/0(新增整组 groupV62,含 P0 两道防线)
· 反向验证 51/0(新增 13 条注入,逐条确认能变红)
· Schema 列守卫 12/0
· **真机 22/0**(6 组,含照片横滑 `1/5 → 2/5`、四个页签、点头像不被踢)
2026-10-19 15:27:31 +08:00

187 lines
7.4 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 · 模块设置浮层(鸿蒙端)
*
* 🔴 用户 2026-10-16:「**所有页面的设置按钮全部都改成和网页版一致**,
* 就是**三个横线**那种设置按钮。」
*
* 网页版的做法(`templates/` 下各模块的 `_settings_menu.php`):
* 每个模块的动作区最右有一个 `<details class="movie-set menu">`,
* `summary` 里就是**三条横线**的 SVG
* (`M2.5 4.5h11M2.5 8h11M2.5 11.5h11`),点开是一个菜单列表,
* 每条**图标 + 文字**。
*
* 🔴 本组件就是那个"点开后的菜单"(鸿蒙端做成底部浮层,与家书其它浮层同一形态)。
* ⚠️ **通用**:各模块只是条目不同,所以做成一个组件 + 传 `items`,
* 而不是给 8 个模块各写一个浮层(那种写法必然漂移)。
*
* ⚠️ 条目点了"还没做"的怎么办:**照实显示为不可用**(灰掉 + "暂未开放"),
* 而不是让点击没反应 —— 后者用户会以为卡了。
*/
import { FbColors, FbMetric, FbIcon, fbTint, FbTint } from '../common/Theme';
import { FbSheetRing } from './FbSheetRing';
import { FbSheetTitleBar } from './FbSheetClose';
/** 贴底卡片的手势区缓冲(vp)—— 同其它浮层
* ⚠️ 2026-10-19 起本浮层改成**居中弹框**,这个常量暂时没有引用;
* 保留它(而不是删掉)是因为"贴底浮层"这个形态本工程其它地方还在用,
* 删了以后有人要改回去还得再想一遍为什么是 16。 */
const FB_SHEET_BOTTOM_GAP: number = 16;
/** 设置菜单里的一条 */
export interface FbSettingItem {
/** 图标名(`FbIcon.ui()` 的 key,如 'chart' / 'gear' / 'filter') */
icon: string;
/** 文字 */
label: string;
/** 一句话说明(可为空) */
hint: string;
/**
* 点了做什么。传空串 = **这项还没做**(画成灰色 + "暂未开放",点了不响应)。
* ⚠️ 用 string 而不是回调:@Param 传函数在 ArkTS 里会被"不可序列化"折腾,
* 这里只需要一个"标识",由父组件的 @Event 统一分发。
*/
action: string;
}
@ComponentV2
export struct FbModuleSettingsSheet {
/** 标题(如「观影设置」) */
@Param title: string = '设置';
/** 菜单条目 */
@Param items: FbSettingItem[] = [];
/** 关闭 */
@Event onClose: () => void = (): void => {
};
/** 点了某条(`action` 是条目的标识) */
@Event onPick: (action: string) => void = (a: string): void => {
};
/**
* 卡片外圈描边环的颜色(hex,'' = 用默认品牌色)。
*
* 🔴 本组件是**通用**的(读书/观影/任务/资源…都用它)⇒ 环色由调用方传
* "那个模块的主题色";不传时用品牌砖红(至少不是灰色,不会看着像禁用)。
*/
@Param accent: string = '';
/** 环色:显式给了就用给的,否则用品牌色 */
private accentColor(): string {
return this.accent !== '' ? this.accent : FbTint.brand;
}
build() {
/* 🔴 2026-10-19:与其它浮层统一成"**居中的圆角弹框 + 主题色环**"
(用户:「所有弹框都加线,保持一致」)。
⚠️ 原来是"贴底满宽 + 只有上面两个角圆",宿主 Stack 的对齐是 `Bottom`
⇒ 它看起来像一条"抽屉",加了环也只会露在屏幕顶部一条。
⚠️ 这是一个**铺满屏幕**的容器 ⇒ 必须给 `hitTestBehavior(Transparent)`:
否则它把整屏的点击都吃掉,"点空白关掉"就失效了。 */
Column() {
Column() {
FbSheetRing({ radius: FbMetric.radius, ringColor: this.accentColor() }) {
Column({ space: 0 }) {
// ---- 标题栏(全应用统一版式:左标题 + 右「完成」,见 FbSheetClose.ets)----
Row() {
FbSheetTitleBar({
title: this.title,
accent: this.accentColor(),
// ⚠️ 设置类是"改完即生效" ⇒ 用「完成」而不是 ✕(见 FbSheetTitleBar 的说明)
confirmText: '完成',
onClose: (): void => {
this.onClose();
}
})
}
.width('100%')
.padding({ left: 18, right: 12, top: 14, bottom: 6 })
Scroll() {
Column({ space: 0 }) {
ForEach(this.items, (it: FbSettingItem) => {
this.settingRow(it)
}, (it: FbSettingItem): string => 'st_' + it.action + '_' + it.label)
}
.width('100%')
.padding({ top: 10 })
}
/* 🔴 2026-10-19:原来是 `Scroll().layoutWeight(1)` —— 它让滚动区**吃满剩余高度**,
于是只有 4 条菜单也会把卡片顶到 `maxHeight`(70%),
下面留一大片空白(真机截图:菜单之后空半屏)。
⇒ `flexShrink(1)`:卡片按内容自适应高,
菜单多到超过 70% 时这一块才收缩并滚动。
⚠️ 不能只删掉 `layoutWeight`:`Column` 的子项默认 `flexShrink = 0`,
不收缩 ⇒ 超长菜单会**溢出卡片**被裁掉。 */
.flexShrink(1)
.width('100%')
.scrollBar(BarState.Off)
.align(Alignment.TopStart)
}
.width('100%')
.constraintSize({ maxHeight: '70%' })
.padding({ left: 18, right: 18, top: 18, bottom: 20 })
.alignItems(HorizontalAlign.Start)
}
}
.width('92%')
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.hitTestBehavior(HitTestMode.Transparent)
}
/**
* 一条菜单项:**图标 + 文字**(照网页版:`每条文字前都要有图标`)。
*
* ⚠️ 未实现的条目(`action === ''`)画灰、且**不响应点击** ——
* "点了没反应"比"明确告诉你还没做"更糟(用户会以为卡死)。
*/
@Builder
settingRow(it: FbSettingItem) {
Row({ space: 12 }) {
Image(FbIcon.ui(it.icon))
.width(17)
.height(17)
.renderMode(ImageRenderMode.Template)
.colorFilter(fbTint(it.action === '' ? FbTint.faint : FbTint.muted))
Column({ space: 1 }) {
Text(it.label)
.fontSize(FbMetric.fontBody)
.fontColor(it.action === '' ? FbColors.faint : FbColors.ink)
if (it.hint !== '') {
Text(it.hint)
.fontSize(FbMetric.fontTiny)
.fontColor(FbColors.faint)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
if (it.action === '') {
Text('暂未开放')
.fontSize(FbMetric.fontTiny)
.fontColor(FbColors.faint)
} else {
Text('›')
.fontSize(FbMetric.fontBody)
.fontColor(FbColors.faint)
}
}
.width('100%')
.padding({ left: 12, right: 12, top: 12, bottom: 12 })
.borderRadius(FbMetric.radiusS)
.margin({ bottom: 6 })
.backgroundColor(it.action === '' ? Color.Transparent : FbColors.inputBg)
.alignItems(VerticalAlign.Center)
.onClick((): void => {
// ⚠️ 未实现的项**点了不响应**(不是"关闭浮层")——
// 否则用户以为"点了进去但没东西"
if (it.action !== '') {
this.onPick(it.action);
}
})
}
}