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:
@@ -100,7 +100,17 @@ export struct FbAccountPage {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const r: FbApiResult = await fbApi('/api/v1/account');
|
||||
/* 🔴🔴 必须把令牌传进去(2026-10-19 事故):
|
||||
这里原来调用时**只给了路径、没给请求选项** —— `token` 变量取了却没用。
|
||||
后果**极严重**:本页在 `aboutToAppear` 里就调它,而 `/account` 是要鉴权的
|
||||
⇒ 请求 401 ⇒ 网络层的全局回调(`fbOnUnauthorized`)立刻**清掉本地令牌
|
||||
+ 踢回登录页** ⇒ 用户表现为"点一下右上角头像,App 就退出登录了"
|
||||
(这就是用户报的"现在还是不稳定,会退出,提示:这台设备已经被退出登录")。
|
||||
⚠️ 同类检查:**任何带 token 变量却不传的 fbApi 调用都是定时炸弹**,
|
||||
因为它不是"这个接口不能用",而是"顺手把你踢下线"。
|
||||
⇒ 已在 `fbApi()` 里加了兜底(本地有令牌、请求又没带时自动补上),
|
||||
并加了静态守卫(`_fb_arkts_guard.js` 的 groupV62)盯这类写法。 */
|
||||
const r: FbApiResult = await fbApi('/api/v1/account', { token: token });
|
||||
if (!r.ok) {
|
||||
this.accountNote = r.error;
|
||||
return;
|
||||
@@ -181,8 +191,9 @@ export struct FbAccountPage {
|
||||
body['password'] = this.pw.fresh;
|
||||
body['password2'] = this.pw.again;
|
||||
try {
|
||||
// 🔴 同 `loadAccount` 那处:令牌必须传(漏传 = 401 = 被踢下线)
|
||||
const r: FbApiResult = await fbApi('/api/v1/account/password',
|
||||
{ method: 'POST', body: body });
|
||||
{ method: 'POST', body: body, token: FB_TOKEN_STORE.get() });
|
||||
if (!r.ok) {
|
||||
this.pwBad = true;
|
||||
this.pwNote = r.error;
|
||||
|
||||
@@ -323,6 +323,23 @@ export struct FbMoviesPage {
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新读一次本地影院列表(影院管理里增/改/删之后调)。
|
||||
*
|
||||
* 🔴 v62:不重读的话,关掉影院浮层后页头上那个「影院 N 家」还是旧数字
|
||||
* (用户刚新增了一家,页头却还写着 26 家 ⇒ 会以为没保存成功)。
|
||||
* ⚠️ 只读本地库(`cinemaList()`)不联网 —— 写操作那边已经回写过本地库了
|
||||
* (见 `FbMovieRepo.saveCinema` 的说明),这里再打一次接口是多余的往返。
|
||||
* ⚠️ 失败**不弹错**:这只是个页头计数,失败了下次同步也会纠正。
|
||||
*/
|
||||
private async reloadCinemas(): Promise<void> {
|
||||
try {
|
||||
this.cinemas = await this.repo.cinemaList();
|
||||
} catch (err) {
|
||||
hilog.warn(DOMAIN, TAG, '重读本地影院失败:%{public}s', JSON.stringify(err));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 后台同步:拉接口 → 落本地库 → 再读一次刷新界面。
|
||||
*
|
||||
@@ -394,8 +411,16 @@ export struct FbMoviesPage {
|
||||
|
||||
FbCinemaSheet({
|
||||
cinemas: this.cinemas,
|
||||
// 🔴 v62:环色/主题色也传(影院管理里的页签、按钮都用观影模块色)
|
||||
accent: this.accent,
|
||||
onClose: (): void => {
|
||||
this.cinemaSheetOpen = false;
|
||||
},
|
||||
/* 🔴 v62(用户 2026-10-19):影院管理里新增/修改/删除之后,
|
||||
要让**本页也拿到新列表** —— 否则关掉浮层,页头上那个
|
||||
「影院 N 家」和影院色点还是旧的(用户会以为没保存成功)。 */
|
||||
onCinemasChanged: (): void => {
|
||||
this.reloadCinemas();
|
||||
}
|
||||
})
|
||||
.transition(fbSheetTransition())
|
||||
|
||||
@@ -70,6 +70,11 @@ function toRosterItem(p: FbRosterPerson): FbRosterItem {
|
||||
roster: 'member',
|
||||
birthday: p.birthdaySolar,
|
||||
isAdmin: p.isAdmin,
|
||||
// 🔴 v62:单位/职务(列表要显示,见 FbRosterItem.org 的说明)。
|
||||
// ⚠️ 直接取 `p.org`/`p.title`(本地库里已有),**不是** `p.jobs[0]` ——
|
||||
// 列表读本地库,而本地的 `jobs` 恒为空(那张表没同步)。
|
||||
org: p.org,
|
||||
title: p.title,
|
||||
};
|
||||
return it;
|
||||
}
|
||||
@@ -436,6 +441,28 @@ export struct FbPeoplePage {
|
||||
: '登录后会从服务器拉取你名册里的人';
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表里那行「单位|职务」。
|
||||
*
|
||||
* 🔴 v62:用户要列表显示单位/职务。取 `jobs[0]`(= 当前/最近那段,
|
||||
* 排序由服务端保证,与详情页一致)。
|
||||
* ⚠️ **不显示起止时间**:列表要的是"他现在在哪、做什么",
|
||||
* 加个「(2020-09 至今)」会让这一行太长、被省略号吃掉职务
|
||||
* (起止在详情页的「工作经历」里有完整显示)。
|
||||
* ⚠️ 只有单位没有职务(或反之)时不要留一个空的分隔符 ——
|
||||
* 用数组 join,缺哪个就少一段。
|
||||
*/
|
||||
private jobLine(p: FbRosterItem): string {
|
||||
const parts: string[] = [];
|
||||
if (p.org !== '') {
|
||||
parts.push(p.org);
|
||||
}
|
||||
if (p.title !== '') {
|
||||
parts.push(p.title);
|
||||
}
|
||||
return parts.join('|');
|
||||
}
|
||||
|
||||
/** 从本地库读名册并刷新界面 */
|
||||
private async reloadFromLocal(): Promise<void> {
|
||||
try {
|
||||
@@ -1164,10 +1191,20 @@ export struct FbPeoplePage {
|
||||
*/
|
||||
@Builder
|
||||
dockSpacerItem() {
|
||||
/* 🔴🔴 v62(用户 2026-10-19):「**名册搜索,搜索后,下面有一个白色的框,
|
||||
什么内容都没有,删除。**」
|
||||
⇒ 就是这个占位块。它**必须留着**(高度要给悬浮栏让位,
|
||||
否则最后一张卡片会被悬浮栏压住),但它的底色原来是 `FbColors.surface`
|
||||
(**白**),而列表底色是 `FbColors.paper`(**米**)⇒
|
||||
列表内容一短(搜索只剩一两条时)它就露出来,看着像"一个什么都没有的白框"。
|
||||
⇒ 改成与列表同色(paper)⇒ 它变成"看不见的空白",
|
||||
预留高度的作用不变,但不再有那个框。
|
||||
⚠️ 别改成 `height(0)` 或删掉:那会让最后一张卡片被悬浮栏压住
|
||||
(这正是当初加它的原因,见 `build` 里关于"内容从悬浮框上面滚出来"的说明)。 */
|
||||
Row()
|
||||
.width('100%')
|
||||
.height(FB_DOCK_RESERVE)
|
||||
.backgroundColor(FbColors.surface)
|
||||
.backgroundColor(FbColors.paper)
|
||||
}
|
||||
|
||||
/** 一个人一行:头像 + 名字/称谓 + 标签 + 生日 */
|
||||
@@ -1218,6 +1255,22 @@ export struct FbPeoplePage {
|
||||
}
|
||||
}
|
||||
.alignItems(VerticalAlign.Center)
|
||||
.width('100%')
|
||||
|
||||
/* 🔴🔴 v62(用户 2026-10-19):「现在名册的列表太空了,应该显示更多内容,
|
||||
例如**工作单位、职务**。」
|
||||
⚠️ **独立成一行**(不是塞进上面标签那行):
|
||||
单位名可以很长("XX大学XX学院XX系"),与标签/生日挤同一行时
|
||||
会被省略号吃掉职务 —— 而"在哪、做什么"正是用户要看的信息。
|
||||
独立一行后,单位名最多占满整行宽度(仍带省略号防溢出)。 */
|
||||
if (this.jobLine(p) !== '') {
|
||||
Text(this.jobLine(p))
|
||||
.fontSize(10)
|
||||
.fontColor(FbColors.muted)
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
.width('100%')
|
||||
}
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
|
||||
Reference in New Issue
Block a user