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

467 lines
16 KiB
Plaintext
Raw Normal View History

/**
* 家书 FamilyBook · 影院管理浮层(鸿蒙端)
*
* 🔴 v63(用户 2026-10-19)**推翻了 v62 的页签方案**,原话:
* 「**影院管理就是影院管理。高级搜索统计和年度报告不在"影院管理"这个功能里**,
* 而是点击观影设置后,弹框里面有"影院管理""高级搜索统计"和"年度报告";
* 点击之后,会进入到搜索统计和年度报告里面。
* **影院管理就是管理影院,不要把统计搜索和年报放在这里面。**」
*
* ⇒ v62 我把四项做成同一个浮层里的四个页签;用户要的是**平级的三个入口**:
* 观影页 ☰ → 设置菜单(影院管理 / 高级搜索、统计 / 年度报告)→ 各自独立浮层。
* ⇒ 本组件**只剩影院本身**:列表 + 新增 / 修改 / 删除。
* 统计 / 搜索 / 年报在 `FbMovieInsightSheet`(由 `mode` 决定显示哪一项)。
*
* ⚠️ 数据来自**本地库**(观影同步时一起落库的 `cinemas` 表),不是现拉接口 ——
* 见 `FbMovieRepo.cinemaList()` 的说明。
* 但**写操作**(增改删)必须联网,写完立刻回写本地库(见 `saveCinema`)。
*/
import { FbColors, FbMetric, fbAlpha } from '../common/Theme';
import { FbModuleDef, fbModuleOf, fbThemeVars } from '../model/Modules';
import { FbCinemaItem, FbMovieRepo } from '../data/FbMovieRepo';
import { FbSheetRing } from './FbSheetRing';
import { FbSheetClose } from './FbSheetClose';
/**
* 贴底卡片的手势区缓冲(vp)。
*
* 🔴 应用开了全屏布局且**隐藏了系统手势条**(见 EntryAbility)——
* 手势区不再有任何视觉提示,贴底的卡片必须自己让出这一段,
* 否则最后一排按钮会被"从下往上滑"误触。
*/
const FB_SHEET_BOTTOM_GAP: number = 16;
/** 新建影院时的默认色(与网页 `Cinema::PALETTE` 里的紫同一档) */
const DEFAULT_CINEMA_COLOR: string = '#7A4E8C';
/** 新建影院时可选的颜色(与网页 `Cinema::PALETTE` 同一族,取"色相差得开"的那几个) */
const PALETTE: string[] = [
'#7A4E8C', '#e84393', '#e91e63', '#f06292',
'#2980b9', '#6c5ce7', '#16a085', '#2ecc71',
'#f8c471', '#a67c52', '#95a5a6', '#7f8c8d'
];
@ComponentV2
export struct FbCinemaSheet {
/** 影院列表(本地库;外部传入,改动后由本组件刷新并回调) */
@Param cinemas: FbCinemaItem[] = [];
/** 关闭回调 */
@Event onClose: () => void = (): void => {
};
/** 影院列表有变化时通知页面刷新(新建/修改/删除之后) */
@Event onCinemasChanged: () => void = (): void => {
};
/**
* 卡片外圈描边环的颜色(hex,'' = 用观影模块色)。
*
* ⚠️ 必须是字符串:环色要直接当背景色(`FbColors.brand` 是 `Resource`,用不了)。
*/
@Param accent: string = '';
// ---- 编辑态 ----
/** 正在编辑的影院 id(0 = 新增;-1 = 没在编辑) */
@Local editId: number = -1;
@Local fName: string = '';
@Local fPlace: string = '';
@Local fAddress: string = '';
@Local fColor: string = DEFAULT_CINEMA_COLOR;
/** 保存中(防连点) */
@Local saving: boolean = false;
/** 一句话结果(成功/失败都写这里,页面上显示一行灰字) */
@Local note: string = '';
/** 待确认删除的影院 id(0 = 没有) */
@Local delId: number = 0;
private repo: FbMovieRepo = FbMovieRepo.inst();
/** 环色:显式给了就用给的,否则取观影模块色(影院属于观影) */
private accentColor(): string {
if (this.accent !== '') {
return this.accent;
}
const m: FbModuleDef | undefined = fbModuleOf('movies');
return fbThemeVars(m !== undefined ? m.color : '#A84A28').brand;
}
/** 开始编辑(id=0 表示新增) */
private startEdit(id: number): void {
if (id === 0) {
this.editId = 0;
this.fName = '';
this.fPlace = '';
this.fAddress = '';
this.fColor = DEFAULT_CINEMA_COLOR;
} else {
const c: FbCinemaItem | undefined =
this.cinemas.find((x: FbCinemaItem): boolean => x.id === id);
if (c === undefined) {
return;
}
this.editId = id;
this.fName = c.name;
this.fPlace = c.place;
this.fAddress = c.address;
this.fColor = c.color;
}
this.note = '';
}
private async save(): Promise<void> {
if (this.saving) {
return;
}
if (this.fName.trim() === '') {
this.note = '影院名称不能为空';
return;
}
this.saving = true;
const err: string = await this.repo.saveCinema(
this.editId, this.fPlace, this.fName, this.fAddress, this.fColor);
this.saving = false;
if (err !== '') {
this.note = err;
return;
}
this.note = this.editId === 0 ? '已新增' : '已保存';
this.editId = -1;
this.onCinemasChanged();
}
private async remove(id: number): Promise<void> {
this.delId = 0;
const err: string = await this.repo.deleteCinema(id);
this.note = err !== '' ? err : '已删除(历史观影记录保留)';
if (err === '') {
this.onCinemasChanged();
}
}
build() {
// FB_WRAP_CENTER:把卡片**垂直居中**(用户 2026-10-16 要求:浮在屏幕中间)。
// ⚠️ 居中放在**组件内部**做,而不是改宿主的 Stack 对齐 ——
// 宿主 Stack 里还有别的子元素,改对齐会连带挪走它们。
// ⚠️ 外层铺满 + Center;内层是那张 92% 宽的圆角卡。
Column() {
Column() {
FbSheetRing({ radius: FbMetric.radius, ringColor: this.accentColor() }) {
Column({ space: 0 }) {
// ---- 标题栏(统一版式:左标题 + 右 ✕,见 FbSheetClose.ets)----
Row({ space: 8 }) {
Column({ space: 2 }) {
Text(this.editId >= 0
? (this.editId === 0 ? '新增影院' : '修改影院') : '影院管理')
.fontSize(FbMetric.fontTitle)
.fontWeight(FontWeight.Medium)
.fontColor(FbColors.ink)
.maxLines(1)
/* 🔴 v63:副标题**不再写「N 家」**(用户:「影院电影列表里面有个
『影院26家』,不知道是什么意思……没有用,要把它删掉」)。
⇒ 换成一句说明"这里能做什么",比一个看不懂的数字有用。 */
Text('管理看过的影院:新增 / 修改 / 删除')
.fontSize(FbMetric.fontTiny)
.fontColor(FbColors.faint)
.maxLines(1)
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.flexShrink(1)
FbSheetClose({
onClose: (): void => {
this.onClose();
},
color: this.accentColor()
})
}
.width('100%')
.alignItems(VerticalAlign.Center)
Divider()
.color(FbColors.line)
.margin({ top: 10, bottom: 4 })
// ---- 内容 ----
// ⚠️ `flexShrink(1)` 让内容**按自身高度收缩**(短内容不撑满 maxHeight),
// 本项目在日程详情/模块设置踩过"卡片空半屏"。
Scroll() {
Column({ space: 0 }) {
this.cinemaPane()
}
.width('100%')
}
.flexShrink(1)
.width('100%')
.scrollBar(BarState.Off)
.align(Alignment.TopStart)
// ---- 底部:一行结果提示 ----
if (this.note !== '') {
Text(this.note)
.fontSize(FbMetric.fontTiny)
.fontColor(FbColors.muted)
.width('100%')
.padding({ top: 8 })
}
}
.width('100%')
.constraintSize({ maxHeight: '76%' })
.padding({ left: 18, right: 18, top: 18, bottom: 20 + FB_SHEET_BOTTOM_GAP })
.alignItems(HorizontalAlign.Start)
}
}
.width('92%')
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.hitTestBehavior(HitTestMode.Transparent)
}
// ==================== 影院:增 / 改 / 删 ====================
@Builder
cinemaPane() {
// ---- 编辑表单(编辑态才显示)----
if (this.editId >= 0) {
Column({ space: 8 }) {
this.field('影院名', '如「横店电影城」', this.fName, (v: string): void => {
this.fName = v;
})
this.field('场所', '如「东明县京都大润发」(可空)', this.fPlace, (v: string): void => {
this.fPlace = v;
})
this.field('地址', '详细地址(可空)', this.fAddress, (v: string): void => {
this.fAddress = v;
})
// 颜色:给一排候选(网页 `Cinema::PALETTE` 是同样的做法)
Row({ space: 8 }) {
Text('颜色')
.fontSize(FbMetric.fontTiny)
.fontColor(FbColors.faint)
.width(52)
Row({ space: 6 }) {
ForEach(PALETTE, (c: string) => {
Column()
.width(20)
.height(20)
.backgroundColor(c)
.borderRadius(10)
.border({ width: this.fColor === c ? 2 : 0, color: FbColors.ink })
.onClick((): void => {
this.fColor = c;
})
}, (c: string): string => 'pal_' + c)
}
.layoutWeight(1)
}
.width('100%')
.alignItems(VerticalAlign.Center)
Row({ space: 8 }) {
Text(this.saving ? '保存中…' : '保存')
.fontSize(FbMetric.fontSmall)
.fontColor(Color.White)
.textAlign(TextAlign.Center)
.layoutWeight(1)
.height(40)
.backgroundColor(this.saving
? fbAlpha(this.accentColor(), 0.5) : this.accentColor())
.borderRadius(FbMetric.radiusS)
.onClick((): void => {
this.save();
})
Text('取消')
.fontSize(FbMetric.fontSmall)
.fontColor(FbColors.muted)
.textAlign(TextAlign.Center)
.layoutWeight(1)
.height(40)
.backgroundColor(FbColors.inputBg)
.borderRadius(FbMetric.radiusS)
.onClick((): void => {
this.editId = -1;
this.note = '';
})
}
.width('100%')
.margin({ top: 2 })
}
.width('100%')
.padding({ top: 13, bottom: 13 })
} else {
// ---- 顶部动作:新增 ----
Row() {
Text('+ 新增影院')
.fontSize(FbMetric.fontSmall)
.fontColor(this.accentColor())
.fontWeight(FontWeight.Medium)
.height(34)
.padding({ left: 12, right: 12 })
.borderRadius(FbMetric.radiusS)
.backgroundColor(fbAlpha(this.accentColor(), 0.10))
.textAlign(TextAlign.Center)
.onClick((): void => {
this.startEdit(0);
})
}
.width('100%')
.padding({ top: 10, bottom: 4 })
}
// ---- 列表 ----
if (this.cinemas.length === 0) {
Text('还没有影院。可以点上面的「新增影院」自己加,'
+ '同步观影记录后去过的地方也会自动出现在这里。')
.fontSize(FbMetric.fontSmall)
.fontColor(FbColors.faint)
.width('100%')
.padding({ top: 28, bottom: 28, left: 12, right: 12 })
.textAlign(TextAlign.Center)
} else {
Column({ space: 0 }) {
ForEach(this.cinemas, (c: FbCinemaItem) => {
Column({ space: 0 }) {
Row({ space: 12 }) {
// 色点:每家影院一个色(与 Web 端一致,便于在别处一眼认出来)
Column()
.width(10)
.height(10)
.backgroundColor(c.color)
.borderRadius(5)
Column({ space: 3 }) {
Row({ space: 8 }) {
Text(c.name)
.fontSize(FbMetric.fontBody)
.fontColor(FbColors.ink)
.maxLines(1)
.flexShrink(1)
if (c.movieCount > 0) {
Text(c.movieCount.toString() + ' 部')
.fontSize(10)
.fontColor(FbColors.faint)
}
}
.alignItems(VerticalAlign.Center)
.width('100%')
// 场所 / 地址(有才显示)
if (c.place !== '' || c.address !== '') {
Text(c.place !== '' ? c.place : c.address)
.fontSize(FbMetric.fontTiny)
.fontColor(FbColors.muted)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.width('100%')
}
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
// ---- 改 / 删 ----
Text('改')
.fontSize(FbMetric.fontTiny)
.fontColor(this.accentColor())
.padding({ left: 8, right: 8, top: 5, bottom: 5 })
.onClick((): void => {
this.startEdit(c.id);
})
Text('删')
.fontSize(FbMetric.fontTiny)
.fontColor(FbColors.red)
.padding({ left: 8, right: 8, top: 5, bottom: 5 })
.onClick((): void => {
this.delId = c.id;
})
}
.width('100%')
.padding({ left: 4, right: 0, top: 11, bottom: 11 })
.alignItems(VerticalAlign.Center)
Divider()
.strokeWidth(0.5)
.color(FbColors.line)
}
.width('100%')
}, (c: FbCinemaItem): string => 'cin_' + c.id.toString())
}
.width('100%')
}
// ---- 删除确认(红底 —— 用户要求"删除按钮全部红色背景")----
if (this.delId > 0) {
this.confirmDelete()
}
}
/** 一行输入(标签 + 输入框) */
@Builder
field(label: string, placeholder: string, value: string, onChange: (v: string) => void) {
Row({ space: 8 }) {
Text(label)
.fontSize(FbMetric.fontTiny)
.fontColor(FbColors.faint)
.width(52)
TextInput({ text: value, placeholder: placeholder })
.placeholderColor(FbColors.faint)
.placeholderFont({ size: FbMetric.fontTiny })
.fontSize(FbMetric.fontSmall)
.fontColor(FbColors.ink)
.backgroundColor(FbColors.inputBg)
.borderRadius(FbMetric.radiusS)
.height(38)
.layoutWeight(1)
.padding({ left: 10, right: 10 })
.onChange((v: string): void => {
onChange(v);
})
}
.width('100%')
.alignItems(VerticalAlign.Center)
}
/** 删除一家影院的二次确认 */
@Builder
confirmDelete() {
Column({ space: 10 }) {
Text('删掉这家影院?')
.fontSize(FbMetric.fontBody)
.fontColor(FbColors.ink)
.fontWeight(FontWeight.Medium)
Text('在这家看过的记录**会保留**(只是不再显示影院名)。')
.fontSize(FbMetric.fontTiny)
.fontColor(FbColors.muted)
.textAlign(TextAlign.Center)
Row({ space: 10 }) {
Text('删除')
.fontSize(FbMetric.fontSmall)
.fontColor(Color.White)
.textAlign(TextAlign.Center)
.layoutWeight(1)
.height(40)
// 🔴 用户 2026-10-19:「删除按钮全部用红色背景。」
.backgroundColor(FbColors.red)
.borderRadius(FbMetric.radiusS)
.onClick((): void => {
this.remove(this.delId);
})
Text('取消')
.fontSize(FbMetric.fontSmall)
.fontColor(FbColors.muted)
.textAlign(TextAlign.Center)
.layoutWeight(1)
.height(40)
.backgroundColor(FbColors.inputBg)
.borderRadius(FbMetric.radiusS)
.onClick((): void => {
this.delId = 0;
})
}
.width('100%')
}
.width('100%')
.padding({ top: 16, bottom: 6 })
.alignItems(HorizontalAlign.Center)
}
}