Files
FamilyBook_harmonyos/entry/src/main/ets/components/FbCinemaPickSheet.ets
T

240 lines
9.0 KiB
Plaintext
Raw Normal View History

/**
* 家书 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() {
// 整屏遮罩 + 居中卡片(与全应用浮层同一形态)
Column() {
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%')
.justifyContent(FlexAlign.Center)
.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);
})
}
}