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`、四个页签、点头像不被踢)
This commit is contained in:
2026-10-19 15:27:31 +08:00
parent 853b0e0b20
commit 4a063114c5
16 changed files with 1905 additions and 215 deletions
+16 -12
View File
@@ -29,6 +29,7 @@ import { FbModuleDef, fbModuleOf, fbThemeVars } from '../model/Modules';
import { FbRosterPerson, FbJobRow, FbContactRow } from '../data/FbPersonRepo';
import { FbAvatar, FbTag } from './Widgets';
import { FbSheetRing } from './FbSheetRing';
import { FbSheetClose } from './FbSheetClose';
import { FB_TOKEN_STORE } from '../data/FbTokenStore';
import { fbImageSrc } from '../data/FbAttachQueue';
@@ -400,11 +401,13 @@ export struct FbPersonSheet {
.fontWeight(FontWeight.Medium)
.fontColor(FbColors.ink)
.maxLines(1)
.flexShrink(1)
if (this.person !== null && this.person.isAdmin) {
FbTag({ text: '管理员', tone: 'brand' })
}
}
.alignItems(VerticalAlign.Center)
.width('100%')
// 本名(昵称与不同名时才有意义)
if (this.person !== null && this.person.name !== this.person.shown) {
@@ -419,9 +422,21 @@ export struct FbPersonSheet {
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
/* 🔴 关闭统一到**右上角**(用户 2026-10-19:全应用只留一种关闭位置)。
⚠️ 与头像**同一行**:原来底部那个「关闭」按钮已删 ——
它和「编辑」并排等宽,用户反馈"分不清哪个是主操作"。
⚠️ `alignItems` 用 `Top`:头像 60 高,✕ 32 高,
居中会让它离卡片顶边太远(看着像浮在名字旁边)。 */
FbSheetClose({
onClose: (): void => {
this.onClose();
},
color: this.accentColor()
})
}
.width('100%')
.alignItems(VerticalAlign.Center)
.alignItems(VerticalAlign.Top)
// ---- 标签 ----
// ⚠️ 标签行**留在头部(不滚动)**:它很短,而且"这人是哪类人"是扫一眼就要的
@@ -540,17 +555,6 @@ export struct FbPersonSheet {
.onClick((): void => {
this.onEdit();
})
Text('关闭')
.fontSize(FbMetric.fontSmall)
.fontColor(FbColors.muted)
.textAlign(TextAlign.Center)
.layoutWeight(1)
.height(40)
.backgroundColor(FbColors.inputBg)
.borderRadius(FbMetric.radiusS)
.onClick((): void => {
this.onClose();
})
}
.width('100%')
.margin({ top: 18 })