四个日期/选择类浮层在真机上**完全打不开**:编译无告警、`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、删遮罩、
热区挪回内层,逐条证明断言真会红)。
243 lines
9.3 KiB
Plaintext
243 lines
9.3 KiB
Plaintext
/**
|
||
* 家书 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);
|
||
})
|
||
}
|
||
} |