Files
FamilyBook_harmonyos/entry/src/main/ets/components/FbCinemaPickSheet.ets
T
laoyang 3b7238a418 v64b:修真机弹层"点了没反应"(根容器 Column → Stack)+ 日期行热区
四个日期/选择类浮层在真机上**完全打不开**:编译无告警、`aboutToAppear` 照常调用、
`dumpLayout` 里连弹层一个节点都看不到、点它会"穿透"到底下的列表。

根因(逐层二分定位):根容器写成 `Column` 且里面**先排一个整高遮罩**
(`Column().width('100%').height('100%').backgroundColor(FB_MASK)`)再排卡片。
`Column` 是**线性**布局 ⇒ 遮罩占满整高后,卡片被排到屏幕**外面**。
⇒ 全屏遮罩与卡片要"叠"(`Stack`),不能"排"(`Column`)。

改动:
- FbDateTimeField / FbCinemaPickSheet / FbPersonPickSheet:根容器改
  `Stack({ alignContent: Alignment.Center })`,遮罩与卡片作并列子项叠放;
  去掉原先用于"居中卡片"的 `justifyContent(FlexAlign.Center)`(Stack 已对齐)。
- FbDateTimeField:「日期行」的 `.onClick` 由内层 `layoutWeight(1)` 的 Row
  挪到**根 Row** —— 挂内层时那一行会被表单所在的**两列容器**盖住,
  点击穿透到底下的观影列表(表现为"点日期行,列表反而响应了")。
- FbSheetClose:`onClose` 由 `@Param` 改 `@Event`(语义归位;实测 `@Param`
  声明回调**并不会失效**,✕ 一直是能点的 —— 这不是本轮 bug 的根因,别混为一谈)。

验证:
- 构建 BUILD SUCCESSFUL;
- 静态守卫 644/0(新增 8 条:三个组件的 Stack 叠放 + 整高遮罩非空转 +
  全项目无"根 Column 先排整高遮罩"坏形态 + 日期行热区在根 Row);
- 守卫反向验证 92/0(新增 6 条注入,含把根 Stack 改回 Column、删遮罩、
  热区挪回内层,逐条证明断言真会红)。
2026-10-20 08:55:27 +08:00

243 lines
9.3 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 · 「选择影院」弹框(鸿蒙端)
*
* 🔴 v64(用户 2026-10-19)原话:
* 「然后另外**影院都应该是弹框选择**…这种选择方式太难找了。」
* 「这个弹框选择要跟**网页端的一样**,在上面有个筛选框。」
*
* ⇒ 观影「记一场」原来把影院做成一排**横向胶囊**(要左右划着找,影院一多就找不到)。
* 网页端(`templates/movies/form.php` 的 `#cinemaPicker`)是:
* **一个按钮 → 弹出列表 → 列表顶部有搜索框 → 点一项选中**。
* 这里按同一形态做,并保留网页端已有的两条细节:
* · 「不记录影院」这一项**恒在列表最上方**(它是"清空选择"的出口,
* 被搜没了用户就取消不掉了 —— 网页端注释里点明过);
* · 搜索匹配「地方 + 名称」(网页端 `data-text` 就是这两段拼的),
* 并且**不区分大小写、按子串**(与网页 `indexOf` 同一口径)。
*
* ⚠️ 为什么与人员选择器**不合并成一个组件**:
* 两者的候选源完全不同(影院有颜色/地方;人有意/头像/拼音首字母),
* 筛选规则也不同(影院按名称子串,人要按拼音**子序列**)。
* 硬合并会变成一个"什么都做、什么都做不专"的组件,且参数会膨胀到十几个。
* 共用的部分已经抽在 `FbSheetRing` / `FbSheetClose` 里了 —— 那才是真正重复的。
*
* ⚠️ 选中即关闭(单选语义):
* 影院是单选(一条观影记录只可能在一家影院看),没有"暂存 + 确定"的必要 ——
* 那一步只会让用户白点一次。多选的是「同看的人」,那边才有暂存区。
*/
import { FbColors, FbMetric, FB_MASK, fbAlpha, fbTint, FbTint } from '../common/Theme';
import { FbModuleDef, fbModuleOf, fbThemeVars } from '../model/Modules';
import { FbCinemaItem } from '../data/FbMovieRepo';
import { FbSheetRing } from './FbSheetRing';
import { FbSheetClose } from './FbSheetClose';
@ComponentV2
export struct FbCinemaPickSheet {
/** 候选影院(本地库,通常几十家) */
@Param cinemas: FbCinemaItem[] = [];
/** 当前选中的影院 id(0 = 不记录影院) */
@Param selected: number = 0;
/** 环色(hex,'' = 观影模块色) */
@Param accent: string = '';
/** 选中一家(0 = 不记录影院) */
@Event onPick: (id: number) => void = (): void => {
};
/** 关闭(不改变选择) */
@Event onClose: () => void = (): void => {
};
@Local keyword: string = '';
private accentColor(): string {
if (this.accent !== '') {
return this.accent;
}
const m: FbModuleDef | undefined = fbModuleOf('movies');
return fbThemeVars(m !== undefined ? m.color : '#A84A28').brand;
}
/** 影院的显示名(「地方 · 名称」;没有地方就只是名称 —— 与网页端同口径) */
private labelOf(c: FbCinemaItem): string {
return c.place !== '' ? (c.place + ' · ' + c.name) : c.name;
}
/**
* 按关键词过滤。
*
* ⚠️ 与网页端 `filter()` **同一口径**:`toLowerCase().indexOf(kw) !== -1`。
* 用子串而不是前缀 —— 用户常常只记得"万象城"却记不全影院名。
* ⚠️ 匹配的是「地方 + 名称」拼起来的串(网页端 `data-text` 就是这个):
* 只匹配名称的话,按"青岛"就搜不出青岛那几家。
*/
private filtered(): FbCinemaItem[] {
const kw: string = this.keyword.trim().toLowerCase();
if (kw === '') {
return this.cinemas;
}
return this.cinemas.filter((c: FbCinemaItem): boolean =>
this.labelOf(c).toLowerCase().indexOf(kw) >= 0);
}
build() {
/* 🔴🔴 必须是 `Stack`,**不能**是 `Column`(2026-10-20 真机实测的真 bug)。
`Column` 是**线性**布局:全屏遮罩(100% 高)会占满整高,
卡片被排到它**下面** ⇒ 落到屏幕之外 ⇒ 弹层"点了没反应"。
症状:编译无告警、dumpLayout 里看不到弹层任何节点、点击会穿透到下层。
⇒ **全屏遮罩与卡片要"叠"(Stack),不能"排"(Column)**。 */
Stack({ alignContent: Alignment.Center }) {
Column()
.width('100%')
.height('100%')
.backgroundColor(FB_MASK)
.onClick((): void => {
this.onClose();
})
Column() {
FbSheetRing({ radius: FbMetric.radius, ringColor: this.accentColor() }) {
Column({ space: 10 }) {
// ---- 标题栏(统一版式:左标题 + 右 ✕)----
Row({ space: 8 }) {
Column({ space: 2 }) {
Text('选择影院')
.fontSize(FbMetric.fontTitle)
.fontWeight(FontWeight.Medium)
.fontColor(FbColors.ink)
.maxLines(1)
Text('共 ' + this.cinemas.length.toString() + ' 家')
.fontSize(FbMetric.fontTiny)
.fontColor(this.accentColor())
.maxLines(1)
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.flexShrink(1)
FbSheetClose({
onClose: (): void => {
this.onClose();
},
color: this.accentColor()
})
}
.width('100%')
.alignItems(VerticalAlign.Center)
// ---- 搜索框(用户点名要"跟网页端一样,上面有个筛选框")----
Row({ space: 6 }) {
Image($r('app.media.fb_ic_search'))
.width(15)
.height(15)
.renderMode(ImageRenderMode.Template)
.colorFilter(fbTint(FbTint.faint))
TextInput({ text: this.keyword, placeholder: '搜影院(地方或名称)' })
.placeholderColor(FbColors.faint)
.placeholderFont({ size: FbMetric.fontTiny })
.fontSize(FbMetric.fontSmall)
.fontColor(FbColors.ink)
.backgroundColor(Color.Transparent)
.height(38)
.layoutWeight(1)
.padding(0)
.onChange((v: string): void => {
this.keyword = v;
})
}
.width('100%')
.padding({ left: 10, right: 10 })
.backgroundColor(FbColors.inputBg)
.borderRadius(FbMetric.radiusS)
.alignItems(VerticalAlign.Center)
// ---- 列表 ----
Scroll() {
Column({ space: 6 }) {
// 「不记录影院」**恒在**(哪怕正在搜索)—— 它是取消选择的唯一出口
this.pickRow(0, '不记录影院', '', true)
ForEach(this.filtered(), (c: FbCinemaItem) => {
this.pickRow(c.id, this.labelOf(c), c.color, false)
}, (c: FbCinemaItem): string => 'cp_' + c.id.toString())
if (this.filtered().length === 0) {
Text('没有匹配的影院')
.fontSize(FbMetric.fontTiny)
.fontColor(FbColors.faint)
.width('100%')
.textAlign(TextAlign.Center)
.padding({ top: 18, bottom: 18 })
}
}
.width('100%')
}
.constraintSize({ maxHeight: 320 })
.width('100%')
.scrollBar(BarState.Auto)
.align(Alignment.TopStart)
}
.width('100%')
.padding({ left: 18, right: 18, top: 18, bottom: 18 })
.alignItems(HorizontalAlign.Start)
}
}
.width('92%')
}
.width('100%')
.height('100%')
.hitTestBehavior(HitTestMode.Transparent)
}
/**
* 一行影院。
*
* @param id 影院 id(0 = 不记录影院)
* @param text 显示名
* @param color 影院主题色(空 = 画一个虚线空心点,与网页端 `is-none` 一致)
* @param isNone 是不是"不记录影院"那一项
*/
@Builder
pickRow(id: number, text: string, color: string, isNone: boolean) {
Row({ space: 10 }) {
// 色点:影院色;「不记录影院」画空心圈
if (isNone || color === '') {
Text('')
.width(12)
.height(12)
.borderRadius(6)
.border({ width: 1.5, color: FbColors.faint })
} else {
Text('')
.width(12)
.height(12)
.borderRadius(6)
.backgroundColor(color)
}
Text(text)
.fontSize(FbMetric.fontSmall)
.fontColor(FbColors.ink)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.layoutWeight(1)
// 选中态:右侧一个对勾(比"整行变色"更明确 —— 列表底色本来就浅)
if (id === this.selected) {
Text('✓')
.fontSize(FbMetric.fontSmall)
.fontColor(this.accentColor())
.fontWeight(FontWeight.Medium)
}
}
.width('100%')
.height(46)
.padding({ left: 12, right: 12 })
.backgroundColor(id === this.selected
? fbAlpha(this.accentColor(), 0.10) : FbColors.inputBg)
.borderRadius(FbMetric.radiusS)
.alignItems(VerticalAlign.Center)
.onClick((): void => {
this.onPick(id);
})
}
}