用户 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`、四个页签、点头像不被踢)
187 lines
7.4 KiB
Plaintext
187 lines
7.4 KiB
Plaintext
/**
|
||
* 家书 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);
|
||
}
|
||
})
|
||
}
|
||
} |