2026-10-18 03:35:39 +08:00
|
|
|
|
/**
|
2026-10-19 15:27:31 +08:00
|
|
|
|
* 家书 FamilyBook · 影院管理浮层(鸿蒙端)
|
2026-10-18 03:35:39 +08:00
|
|
|
|
*
|
2026-10-19 15:27:31 +08:00
|
|
|
|
* 🔴 v62(用户 2026-10-19)原话:
|
|
|
|
|
|
* 「设置里面的影院管理,这些功能都还没有,应该补充,就是增加影院呢、修改影院呢、
|
|
|
|
|
|
* 高级搜索、统计、年度报告,这些全部都应该补充进去。」
|
2026-10-18 03:35:39 +08:00
|
|
|
|
*
|
2026-10-19 15:27:31 +08:00
|
|
|
|
* ⇒ 之前这个浮层**只是一个只读列表**(连"新增"都没有),而网页端
|
|
|
|
|
|
* `templates/movies/` 下早就有 `cinemas.php` / `search.php` / `stats.php` /
|
|
|
|
|
|
* `report.php` 四页。这一版把端上补齐到与网页同一套能力:
|
|
|
|
|
|
* · 顶部一排「页签」:影院 / 统计 / 搜索 / 年报;
|
|
|
|
|
|
* · 影院页:**新增 / 修改 / 删除**(服务端 CRUD 早就有,端上没接);
|
|
|
|
|
|
* · 统计页:总览 + 按影院计数(`/api/v1/movies/stats`);
|
|
|
|
|
|
* · 搜索页:片名 / 影院 / 人 / 起止日期(`/api/v1/movies/search`);
|
|
|
|
|
|
* · 年报页:按年切换(`/api/v1/movies/report`)。
|
2026-10-18 03:35:39 +08:00
|
|
|
|
*
|
2026-10-19 15:27:31 +08:00
|
|
|
|
* ⚠️ 四个页签**共用一个浮层**(而不是四个独立浮层):
|
|
|
|
|
|
* 它们是"同一个东西的不同视角",来回切换不该经过"关掉再打开"(那会闪一下)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 🔴 数据来源分两路(有意的):
|
|
|
|
|
|
* · **影院列表**读**本地库**(`cinemas` 表)—— 与观影页其它内容一致,断网也看得见;
|
|
|
|
|
|
* · **统计 / 年报 / 搜索**必须**联网**——它们的口径要与网页**完全一致**
|
|
|
|
|
|
* (同一份 `MovieStats`)。本地库算出来的是"已同步的那部分",
|
|
|
|
|
|
* 少同步一次就少一部,用户会发现"网页 124 部、App 121 部"。
|
|
|
|
|
|
* ⇒ 这三页断网时显示"需要联网",而不是给一个偏低的数字。
|
2026-10-18 03:35:39 +08:00
|
|
|
|
*/
|
|
|
|
|
|
|
2026-10-19 15:27:31 +08:00
|
|
|
|
import { FbColors, FbMetric, fbAlpha } from '../common/Theme';
|
2026-10-19 08:29:44 +08:00
|
|
|
|
import { FbModuleDef, fbModuleOf, fbThemeVars } from '../model/Modules';
|
2026-10-19 15:27:31 +08:00
|
|
|
|
import {
|
|
|
|
|
|
FbApiMovieSearchRow, FbCinemaItem, FbMovieCinemaStat, FbMovieMonthStat,
|
|
|
|
|
|
FbMovieOverview, FbMovieReport, FbMovieRepo, FbMovieSearchParams, FbMovieStats,
|
|
|
|
|
|
FbMovieYearStat
|
|
|
|
|
|
} from '../data/FbMovieRepo';
|
|
|
|
|
|
import { FB_TOKEN_STORE } from '../data/FbTokenStore';
|
2026-10-19 08:29:44 +08:00
|
|
|
|
import { FbSheetRing } from './FbSheetRing';
|
2026-10-19 15:27:31 +08:00
|
|
|
|
import { FbSheetClose } from './FbSheetClose';
|
2026-10-18 03:35:39 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 贴底卡片的手势区缓冲(vp)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 🔴 应用开了全屏布局且**隐藏了系统手势条**(见 EntryAbility)——
|
|
|
|
|
|
* 手势区不再有任何视觉提示,贴底的卡片必须自己让出这一段,
|
|
|
|
|
|
* 否则最后一排按钮会被"从下往上滑"误触。
|
|
|
|
|
|
*/
|
|
|
|
|
|
const FB_SHEET_BOTTOM_GAP: number = 16;
|
|
|
|
|
|
|
2026-10-19 15:27:31 +08:00
|
|
|
|
/** 四个页签 */
|
|
|
|
|
|
const TABS: string[] = ['影院', '统计', '搜索', '年报'];
|
|
|
|
|
|
|
|
|
|
|
|
/** 新增影院时的默认色(与网页 `Cinema::PALETTE` 里的紫同一档,便于区分新建的) */
|
|
|
|
|
|
const DEFAULT_CINEMA_COLOR: string = '#7A4E8C';
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 空的报告统计对象。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ ArkTS 的 `arkts-no-untyped-obj-literals` **不允许空对象字面量**
|
|
|
|
|
|
* (`return {}` 直接编译不过)⇒ 用一个显式声明的空 interface 顶掉。
|
|
|
|
|
|
*/
|
|
|
|
|
|
interface FbEmptyStats {
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const EMPTY_STATS: FbEmptyStats = {};
|
|
|
|
|
|
|
|
|
|
|
|
/** 同理:总览的空值(`return {}` 编译不过) */
|
|
|
|
|
|
const EMPTY_OVERVIEW: FbMovieOverview = {};
|
2026-10-18 03:35:39 +08:00
|
|
|
|
|
|
|
|
|
|
@ComponentV2
|
|
|
|
|
|
export struct FbCinemaSheet {
|
2026-10-19 15:27:31 +08:00
|
|
|
|
/** 影院列表(本地库;外部传入,改动后由本组件刷新并回调) */
|
2026-10-18 03:35:39 +08:00
|
|
|
|
@Param cinemas: FbCinemaItem[] = [];
|
|
|
|
|
|
/** 关闭回调 */
|
|
|
|
|
|
@Event onClose: () => void = (): void => {
|
|
|
|
|
|
};
|
2026-10-19 15:27:31 +08:00
|
|
|
|
/** 影院列表有变化时通知页面刷新(新建/修改/删除之后) */
|
|
|
|
|
|
@Event onCinemasChanged: () => void = (): void => {
|
|
|
|
|
|
};
|
2026-10-18 03:35:39 +08:00
|
|
|
|
|
2026-10-19 08:29:44 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 卡片外圈描边环的颜色(hex,'' = 用观影模块色)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 必须是字符串:环色要直接当背景色(`FbColors.brand` 是 `Resource`,用不了)。
|
|
|
|
|
|
*/
|
|
|
|
|
|
@Param accent: string = '';
|
|
|
|
|
|
|
2026-10-19 15:27:31 +08:00
|
|
|
|
/** 当前页签(0=影院 1=统计 2=搜索 3=年报) */
|
|
|
|
|
|
@Local tab: number = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// ---- 编辑态(影院页)----
|
|
|
|
|
|
/** 正在编辑的影院 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;
|
|
|
|
|
|
|
|
|
|
|
|
// ---- 统计 / 年报 / 搜索的加载态 ----
|
|
|
|
|
|
@Local statsLoading: boolean = false;
|
|
|
|
|
|
@Local stats: FbMovieStats | null = null;
|
|
|
|
|
|
@Local statsError: string = '';
|
|
|
|
|
|
|
|
|
|
|
|
@Local reportLoading: boolean = false;
|
|
|
|
|
|
@Local report: FbMovieReport | null = null;
|
|
|
|
|
|
@Local reportError: string = '';
|
|
|
|
|
|
/** 年报年份(0 = 用服务端返回的默认年) */
|
|
|
|
|
|
@Local reportYear: number = 0;
|
|
|
|
|
|
|
|
|
|
|
|
@Local searchLoading: boolean = false;
|
|
|
|
|
|
@Local searchRows: FbApiMovieSearchRow[] = [];
|
|
|
|
|
|
@Local searchError: string = '';
|
|
|
|
|
|
/** 搜索条件 */
|
|
|
|
|
|
@Local sTitle: string = '';
|
|
|
|
|
|
@Local sCinemaId: number = 0;
|
|
|
|
|
|
@Local sFrom: string = '';
|
|
|
|
|
|
@Local sTo: string = '';
|
|
|
|
|
|
|
|
|
|
|
|
private repo: FbMovieRepo = FbMovieRepo.inst();
|
|
|
|
|
|
|
2026-10-19 08:29:44 +08:00
|
|
|
|
/** 环色:显式给了就用给的,否则取观影模块色(影院属于观影) */
|
|
|
|
|
|
private accentColor(): string {
|
|
|
|
|
|
if (this.accent !== '') {
|
|
|
|
|
|
return this.accent;
|
|
|
|
|
|
}
|
|
|
|
|
|
const m: FbModuleDef | undefined = fbModuleOf('movies');
|
|
|
|
|
|
return fbThemeVars(m !== undefined ? m.color : '#A84A28').brand;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-19 15:27:31 +08:00
|
|
|
|
/** 切页签时按需拉数据(进统计/年报/搜索才打接口,不在打开浮层就打三个) */
|
|
|
|
|
|
private switchTab(i: number): void {
|
|
|
|
|
|
this.tab = i;
|
|
|
|
|
|
this.note = '';
|
|
|
|
|
|
if (i === 1 && this.stats === null) {
|
|
|
|
|
|
this.loadStats();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (i === 3 && this.report === null) {
|
|
|
|
|
|
this.loadReport(0);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async loadStats(): Promise<void> {
|
|
|
|
|
|
const token: string = FB_TOKEN_STORE.get();
|
|
|
|
|
|
if (token === '') {
|
|
|
|
|
|
this.statsError = '还没登录';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.statsLoading = true;
|
|
|
|
|
|
this.statsError = '';
|
|
|
|
|
|
const r: FbMovieStats | null = await this.repo.stats(token);
|
|
|
|
|
|
this.statsLoading = false;
|
|
|
|
|
|
if (r === null) {
|
|
|
|
|
|
this.statsError = '统计需要联网(本地库只有已同步的一部分,算不准)';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.stats = r;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async loadReport(year: number): Promise<void> {
|
|
|
|
|
|
const token: string = FB_TOKEN_STORE.get();
|
|
|
|
|
|
if (token === '') {
|
|
|
|
|
|
this.reportError = '还没登录';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.reportLoading = true;
|
|
|
|
|
|
this.reportError = '';
|
|
|
|
|
|
const r: FbMovieReport | null = await this.repo.report(token, year);
|
|
|
|
|
|
this.reportLoading = false;
|
|
|
|
|
|
if (r === null) {
|
|
|
|
|
|
this.reportError = '年度报告需要联网';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.report = r;
|
|
|
|
|
|
const y: number | undefined = r.year;
|
|
|
|
|
|
this.reportYear = y !== undefined ? y : year;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async doSearch(): Promise<void> {
|
|
|
|
|
|
const token: string = FB_TOKEN_STORE.get();
|
|
|
|
|
|
if (token === '') {
|
|
|
|
|
|
this.searchError = '还没登录';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const p: FbMovieSearchParams = {
|
|
|
|
|
|
title: this.sTitle.trim(),
|
|
|
|
|
|
cinemaId: this.sCinemaId,
|
|
|
|
|
|
personId: 0,
|
|
|
|
|
|
dateFrom: this.sFrom.trim(),
|
|
|
|
|
|
dateTo: this.sTo.trim()
|
|
|
|
|
|
};
|
|
|
|
|
|
// ⚠️ 与网页同规则:条件全空就不查(否则等于"列出所有观影",不是搜索)
|
|
|
|
|
|
if (p.title === '' && p.cinemaId <= 0 && p.dateFrom === '' && p.dateTo === '') {
|
|
|
|
|
|
this.searchError = '先设一个条件再搜(片名 / 影院 / 日期)';
|
|
|
|
|
|
this.searchRows = [];
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.searchLoading = true;
|
|
|
|
|
|
this.searchError = '';
|
|
|
|
|
|
this.searchRows = await this.repo.search(token, p);
|
|
|
|
|
|
this.searchLoading = false;
|
|
|
|
|
|
if (this.searchRows.length === 0) {
|
|
|
|
|
|
this.searchError = '没有符合条件的观影记录';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 开始编辑(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();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-18 03:35:39 +08:00
|
|
|
|
build() {
|
|
|
|
|
|
// FB_WRAP_CENTER:把卡片**垂直居中**(用户 2026-10-16 要求:浮在屏幕中间)。
|
|
|
|
|
|
// ⚠️ 居中放在**组件内部**做,而不是改宿主的 Stack 对齐 ——
|
|
|
|
|
|
// 宿主 Stack 里还有别的子元素(日程页的悬浮 +),改对齐会连带挪走它们。
|
|
|
|
|
|
// ⚠️ 外层铺满 + Center;内层仍是那张 92% 宽的圆角卡。
|
|
|
|
|
|
Column() {
|
2026-10-19 08:29:44 +08:00
|
|
|
|
// 🔴 2026-10-19:卡片外面加**主题色环**(用户:「所有弹框都加线」),
|
|
|
|
|
|
// 由共用组件 `FbSheetRing` 统一给;这一层只负责 92% 宽度。
|
|
|
|
|
|
Column() {
|
|
|
|
|
|
FbSheetRing({ radius: FbMetric.radius, ringColor: this.accentColor() }) {
|
|
|
|
|
|
Column({ space: 0 }) {
|
2026-10-19 15:27:31 +08:00
|
|
|
|
// ---- 标题栏(统一版式:左标题 + 右 ✕,见 FbSheetClose.ets)----
|
|
|
|
|
|
Row({ space: 8 }) {
|
2026-10-19 08:29:44 +08:00
|
|
|
|
Column({ space: 2 }) {
|
2026-10-19 15:27:31 +08:00
|
|
|
|
Text(this.titleText())
|
2026-10-19 08:29:44 +08:00
|
|
|
|
.fontSize(FbMetric.fontTitle)
|
|
|
|
|
|
.fontWeight(FontWeight.Medium)
|
|
|
|
|
|
.fontColor(FbColors.ink)
|
2026-10-19 15:27:31 +08:00
|
|
|
|
.maxLines(1)
|
|
|
|
|
|
Text(this.subtitleText())
|
2026-10-19 08:29:44 +08:00
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(FbColors.faint)
|
2026-10-19 15:27:31 +08:00
|
|
|
|
.maxLines(1)
|
2026-10-19 08:29:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
.alignItems(HorizontalAlign.Start)
|
|
|
|
|
|
.layoutWeight(1)
|
2026-10-19 15:27:31 +08:00
|
|
|
|
.flexShrink(1)
|
2026-10-18 03:35:39 +08:00
|
|
|
|
|
2026-10-19 15:27:31 +08:00
|
|
|
|
FbSheetClose({
|
|
|
|
|
|
onClose: (): void => {
|
2026-10-19 08:29:44 +08:00
|
|
|
|
this.onClose();
|
2026-10-19 15:27:31 +08:00
|
|
|
|
},
|
|
|
|
|
|
color: this.accentColor()
|
|
|
|
|
|
})
|
2026-10-19 08:29:44 +08:00
|
|
|
|
}
|
2026-10-18 03:35:39 +08:00
|
|
|
|
.width('100%')
|
2026-10-19 08:29:44 +08:00
|
|
|
|
.alignItems(VerticalAlign.Center)
|
|
|
|
|
|
|
2026-10-19 15:27:31 +08:00
|
|
|
|
// ---- 页签 ----
|
|
|
|
|
|
this.tabBar()
|
|
|
|
|
|
|
2026-10-19 08:29:44 +08:00
|
|
|
|
Divider()
|
|
|
|
|
|
.color(FbColors.line)
|
2026-10-19 15:27:31 +08:00
|
|
|
|
.margin({ top: 4, bottom: 4 })
|
2026-10-19 08:29:44 +08:00
|
|
|
|
|
2026-10-19 15:27:31 +08:00
|
|
|
|
// ---- 内容区 ----
|
|
|
|
|
|
// ⚠️ `flexShrink(1)` 让内容**按自身高度收缩**(短内容不撑满 maxHeight)——
|
|
|
|
|
|
// 本项目在日程详情/模块设置踩过"卡片空半屏"(`layoutWeight(1)` 会吃满)。
|
|
|
|
|
|
Scroll() {
|
|
|
|
|
|
Column({ space: 0 }) {
|
|
|
|
|
|
if (this.tab === 0) {
|
|
|
|
|
|
this.cinemaPane()
|
|
|
|
|
|
} else if (this.tab === 1) {
|
|
|
|
|
|
this.statsPane()
|
|
|
|
|
|
} else if (this.tab === 2) {
|
|
|
|
|
|
this.searchPane()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.reportPane()
|
|
|
|
|
|
}
|
2026-10-18 03:35:39 +08:00
|
|
|
|
}
|
2026-10-19 08:29:44 +08:00
|
|
|
|
.width('100%')
|
2026-10-19 15:27:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
.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 })
|
2026-10-19 08:29:44 +08:00
|
|
|
|
}
|
2026-10-18 03:35:39 +08:00
|
|
|
|
}
|
2026-10-19 15:27:31 +08:00
|
|
|
|
// 🔴 用户 2026-10-16:「它应该弹出一个**圆角的、浮在上面的卡片**……
|
|
|
|
|
|
// **不要让它左右充满整个屏幕,下面也不要挨着屏幕**,
|
|
|
|
|
|
// 而是放在**屏幕中间**,采用**圆角浮窗**式的形式。」
|
|
|
|
|
|
// ⇒ 不再贴底满宽:左右留边、四角圆角、垂直居中(由宿主的 Stack 对齐)。
|
2026-10-19 08:29:44 +08:00
|
|
|
|
.width('100%')
|
2026-10-19 15:27:31 +08:00
|
|
|
|
.constraintSize({ maxHeight: '76%' })
|
2026-10-19 08:29:44 +08:00
|
|
|
|
.padding({ left: 18, right: 18, top: 18, bottom: 20 + FB_SHEET_BOTTOM_GAP })
|
|
|
|
|
|
.alignItems(HorizontalAlign.Start)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-10-18 03:35:39 +08:00
|
|
|
|
.width('92%')
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.height('100%')
|
|
|
|
|
|
.justifyContent(FlexAlign.Center)
|
|
|
|
|
|
.hitTestBehavior(HitTestMode.Transparent)
|
|
|
|
|
|
}
|
2026-10-19 15:27:31 +08:00
|
|
|
|
|
|
|
|
|
|
/** 标题(随页签变,用户一眼知道自己在哪一页) */
|
|
|
|
|
|
private titleText(): string {
|
|
|
|
|
|
if (this.tab === 1) {
|
|
|
|
|
|
return '观影统计'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (this.tab === 2) {
|
|
|
|
|
|
return '高级搜索'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (this.tab === 3) {
|
|
|
|
|
|
return '年度报告'
|
|
|
|
|
|
}
|
|
|
|
|
|
return this.editId >= 0 ? (this.editId === 0 ? '新增影院' : '修改影院') : '影院管理';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private subtitleText(): string {
|
|
|
|
|
|
if (this.tab === 1) {
|
|
|
|
|
|
return '与网页端口径一致(需要联网)';
|
|
|
|
|
|
}
|
|
|
|
|
|
if (this.tab === 2) {
|
|
|
|
|
|
return '片名 / 影院 / 日期组合查';
|
|
|
|
|
|
}
|
|
|
|
|
|
if (this.tab === 3) {
|
|
|
|
|
|
return '以我为中心,只算我参与过的场次';
|
|
|
|
|
|
}
|
|
|
|
|
|
return this.cinemas.length.toString() + ' 家 · 按名字排序';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 四个页签(选中用主题色实底 + 白字,与顶栏分段同一套观感) */
|
|
|
|
|
|
@Builder
|
|
|
|
|
|
tabBar() {
|
|
|
|
|
|
Row({ space: 4 }) {
|
|
|
|
|
|
ForEach(TABS, (t: string, i: number) => {
|
|
|
|
|
|
Text(t)
|
|
|
|
|
|
.fontSize(FbMetric.fontSmall)
|
|
|
|
|
|
.fontColor(this.tab === i ? Color.White : FbColors.muted)
|
|
|
|
|
|
.fontWeight(this.tab === i ? FontWeight.Medium : FontWeight.Normal)
|
|
|
|
|
|
.textAlign(TextAlign.Center)
|
|
|
|
|
|
.layoutWeight(1)
|
|
|
|
|
|
.height(32)
|
|
|
|
|
|
.backgroundColor(this.tab === i ? this.accentColor() : Color.Transparent)
|
|
|
|
|
|
.borderRadius(FbMetric.radiusPill)
|
|
|
|
|
|
.onClick((): void => {
|
|
|
|
|
|
this.switchTab(i);
|
|
|
|
|
|
})
|
|
|
|
|
|
}, (t: string): string => 'cintab_' + t)
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding(3)
|
|
|
|
|
|
.backgroundColor(FbColors.inputBg)
|
|
|
|
|
|
.borderRadius(FbMetric.radiusPill)
|
|
|
|
|
|
.margin({ top: 10 })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== ① 影院:增 / 改 / 删 ====================
|
|
|
|
|
|
|
|
|
|
|
|
@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)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== ② 统计 ====================
|
|
|
|
|
|
|
|
|
|
|
|
@Builder
|
|
|
|
|
|
statsPane() {
|
|
|
|
|
|
if (this.statsLoading) {
|
|
|
|
|
|
this.loadingRow('正在取统计…')
|
|
|
|
|
|
} else if (this.statsError !== '') {
|
|
|
|
|
|
this.errorRow(this.statsError, (): void => {
|
|
|
|
|
|
this.loadStats();
|
|
|
|
|
|
})
|
|
|
|
|
|
} else if (this.stats !== null) {
|
|
|
|
|
|
// ---- 总览 ----
|
|
|
|
|
|
this.sectionTitle('总览')
|
|
|
|
|
|
Row({ space: 0 }) {
|
|
|
|
|
|
this.statCell('看过', this.ov().movies)
|
|
|
|
|
|
this.statCell('影院', this.ov().cinemas)
|
|
|
|
|
|
this.statCell('参与的人', this.ov().persons)
|
|
|
|
|
|
this.statCell('人次', this.ov().viewership)
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding({ top: 6, bottom: 10 })
|
|
|
|
|
|
|
|
|
|
|
|
if (this.spanText() !== '') {
|
|
|
|
|
|
Text(this.spanText())
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(FbColors.faint)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---- 按影院 ----
|
|
|
|
|
|
if (this.cinemaStats().length > 0) {
|
|
|
|
|
|
this.sectionTitle('按影院')
|
|
|
|
|
|
Column({ space: 6 }) {
|
|
|
|
|
|
ForEach(this.cinemaStats(), (c: FbMovieCinemaStat) => {
|
|
|
|
|
|
this.barRow(
|
|
|
|
|
|
c.name !== undefined && c.name !== '' ? c.name : '(未命名)',
|
|
|
|
|
|
c.cnt !== undefined ? c.cnt : 0,
|
|
|
|
|
|
this.maxCinemaCount(),
|
|
|
|
|
|
c.color !== undefined && c.color !== '' ? c.color : this.accentColor())
|
|
|
|
|
|
}, (c: FbMovieCinemaStat): string => 'cs_' + (c.id !== undefined ? c.id : 0).toString())
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding({ top: 4 })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---- 按年 ----
|
|
|
|
|
|
if (this.yearStats().length > 0) {
|
|
|
|
|
|
this.sectionTitle('按年')
|
|
|
|
|
|
Column({ space: 6 }) {
|
|
|
|
|
|
ForEach(this.yearStats(), (y: FbMovieYearStat) => {
|
|
|
|
|
|
this.barRow(
|
|
|
|
|
|
(y.year !== undefined ? y.year.toString() : '') + ' 年',
|
|
|
|
|
|
y.cnt !== undefined ? y.cnt : 0,
|
|
|
|
|
|
this.maxYearCount(),
|
|
|
|
|
|
this.accentColor())
|
|
|
|
|
|
}, (y: FbMovieYearStat): string => 'ys_' + (y.year !== undefined ? y.year : 0).toString())
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding({ top: 4 })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---- 按月(近 12 个月)----
|
|
|
|
|
|
if (this.monthStats().length > 0) {
|
|
|
|
|
|
this.sectionTitle('近 12 个月')
|
|
|
|
|
|
Column({ space: 6 }) {
|
|
|
|
|
|
ForEach(this.monthStats(), (m: FbMovieMonthStat) => {
|
|
|
|
|
|
this.barRow(
|
|
|
|
|
|
m.label !== undefined && m.label !== ''
|
|
|
|
|
|
? m.label : (m.ym !== undefined ? m.ym : ''),
|
|
|
|
|
|
m.cnt !== undefined ? m.cnt : 0,
|
|
|
|
|
|
this.maxMonthCount(),
|
|
|
|
|
|
this.accentColor())
|
|
|
|
|
|
}, (m: FbMovieMonthStat): string =>
|
|
|
|
|
|
'ms_' + (m.ym !== undefined ? m.ym : (m.label !== undefined ? m.label : '')))
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding({ top: 4 })
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 总览的安全取值 */
|
|
|
|
|
|
private ov(): FbMovieOverview {
|
|
|
|
|
|
if (this.stats === null || this.stats.overview === undefined) {
|
|
|
|
|
|
return EMPTY_OVERVIEW;
|
|
|
|
|
|
}
|
|
|
|
|
|
return this.stats.overview;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private spanText(): string {
|
|
|
|
|
|
const o: FbMovieOverview = this.ov();
|
|
|
|
|
|
const f: string | null | undefined = o.first_date;
|
|
|
|
|
|
const l: string | null | undefined = o.last_date;
|
|
|
|
|
|
if (f === undefined || f === null || f === '') {
|
|
|
|
|
|
return '';
|
|
|
|
|
|
}
|
|
|
|
|
|
return '从 ' + f + ' 到 ' + (l !== undefined && l !== null && l !== '' ? l : '今');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private cinemaStats(): FbMovieCinemaStat[] {
|
|
|
|
|
|
return this.stats !== null && this.stats.by_cinema !== undefined ? this.stats.by_cinema : [];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private yearStats(): FbMovieYearStat[] {
|
|
|
|
|
|
return this.stats !== null && this.stats.by_year !== undefined ? this.stats.by_year : [];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private monthStats(): FbMovieMonthStat[] {
|
|
|
|
|
|
return this.stats !== null && this.stats.by_month !== undefined ? this.stats.by_month : [];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private maxCinemaCount(): number {
|
|
|
|
|
|
let m: number = 1;
|
|
|
|
|
|
for (const c of this.cinemaStats()) {
|
|
|
|
|
|
const n: number = c.cnt !== undefined ? c.cnt : 0;
|
|
|
|
|
|
if (n > m) {
|
|
|
|
|
|
m = n;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return m;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private maxYearCount(): number {
|
|
|
|
|
|
let m: number = 1;
|
|
|
|
|
|
for (const y of this.yearStats()) {
|
|
|
|
|
|
const n: number = y.cnt !== undefined ? y.cnt : 0;
|
|
|
|
|
|
if (n > m) {
|
|
|
|
|
|
m = n;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return m;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private maxMonthCount(): number {
|
|
|
|
|
|
let m: number = 1;
|
|
|
|
|
|
for (const y of this.monthStats()) {
|
|
|
|
|
|
const n: number = y.cnt !== undefined ? y.cnt : 0;
|
|
|
|
|
|
if (n > m) {
|
|
|
|
|
|
m = n;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return m;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 总览的一格(大数字 + 小标签) */
|
|
|
|
|
|
@Builder
|
|
|
|
|
|
statCell(label: string, value: number | undefined) {
|
|
|
|
|
|
Column({ space: 2 }) {
|
|
|
|
|
|
Text(value !== undefined ? value.toString() : '0')
|
|
|
|
|
|
.fontSize(22)
|
|
|
|
|
|
.fontWeight(FontWeight.Medium)
|
|
|
|
|
|
.fontColor(FbColors.ink)
|
|
|
|
|
|
Text(label)
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(FbColors.faint)
|
|
|
|
|
|
}
|
|
|
|
|
|
.layoutWeight(1)
|
|
|
|
|
|
.alignItems(HorizontalAlign.Center)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 一条横向条形统计(名字 + 条 + 数字)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 条的宽度用 `layoutWeight` 按**比例**分配,而不是直接给百分比字符串:
|
|
|
|
|
|
* ArkTS 里 `width('62%')` 要拼字符串,且宽度为 0 的条会塌成一条线。
|
|
|
|
|
|
* 这里用"按 max 归一后的整数份数"再 `layoutWeight`,0 的时候
|
|
|
|
|
|
* 仍保留一个极小的占位(否则那一行看起来缺了一块)。
|
|
|
|
|
|
*/
|
|
|
|
|
|
@Builder
|
|
|
|
|
|
barRow(label: string, value: number, max: number, color: string) {
|
|
|
|
|
|
Row({ space: 8 }) {
|
|
|
|
|
|
Text(label)
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(FbColors.muted)
|
|
|
|
|
|
.maxLines(1)
|
|
|
|
|
|
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
|
|
|
|
|
.width(72)
|
|
|
|
|
|
Row() {
|
|
|
|
|
|
Row()
|
|
|
|
|
|
.width(Math.max(4, Math.round((value / max) * 100)))
|
|
|
|
|
|
.height(8)
|
|
|
|
|
|
.backgroundColor(color)
|
|
|
|
|
|
.borderRadius(4)
|
|
|
|
|
|
Blank()
|
|
|
|
|
|
}
|
|
|
|
|
|
.layoutWeight(1)
|
|
|
|
|
|
.height(8)
|
|
|
|
|
|
.alignItems(VerticalAlign.Center)
|
|
|
|
|
|
|
|
|
|
|
|
Text(value.toString())
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(FbColors.ink)
|
|
|
|
|
|
.width(30)
|
|
|
|
|
|
.textAlign(TextAlign.End)
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.alignItems(VerticalAlign.Center)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== ③ 高级搜索 ====================
|
|
|
|
|
|
|
|
|
|
|
|
@Builder
|
|
|
|
|
|
searchPane() {
|
|
|
|
|
|
Column({ space: 8 }) {
|
|
|
|
|
|
this.field('片名', '含这几个字就算命中', this.sTitle, (v: string): void => {
|
|
|
|
|
|
this.sTitle = v;
|
|
|
|
|
|
})
|
|
|
|
|
|
// 影院:从本地影院列表里选(不额外打接口)
|
|
|
|
|
|
Row({ space: 8 }) {
|
|
|
|
|
|
Text('影院')
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(FbColors.faint)
|
|
|
|
|
|
.width(52)
|
|
|
|
|
|
Scroll() {
|
|
|
|
|
|
Row({ space: 6 }) {
|
|
|
|
|
|
Text('不限')
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(this.sCinemaId === 0 ? Color.White : FbColors.muted)
|
|
|
|
|
|
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
|
|
|
|
|
|
.backgroundColor(this.sCinemaId === 0 ? this.accentColor() : FbColors.inputBg)
|
|
|
|
|
|
.borderRadius(FbMetric.radiusPill)
|
|
|
|
|
|
.onClick((): void => {
|
|
|
|
|
|
this.sCinemaId = 0;
|
|
|
|
|
|
})
|
|
|
|
|
|
ForEach(this.cinemas, (c: FbCinemaItem) => {
|
|
|
|
|
|
Text(c.name)
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(this.sCinemaId === c.id ? Color.White : FbColors.muted)
|
|
|
|
|
|
.maxLines(1)
|
|
|
|
|
|
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
|
|
|
|
|
|
.backgroundColor(this.sCinemaId === c.id ? this.accentColor() : FbColors.inputBg)
|
|
|
|
|
|
.borderRadius(FbMetric.radiusPill)
|
|
|
|
|
|
.onClick((): void => {
|
|
|
|
|
|
this.sCinemaId = c.id;
|
|
|
|
|
|
})
|
|
|
|
|
|
}, (c: FbCinemaItem): string => 'sch_' + c.id.toString())
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
.scrollable(ScrollDirection.Horizontal)
|
|
|
|
|
|
.scrollBar(BarState.Off)
|
|
|
|
|
|
.layoutWeight(1)
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.alignItems(VerticalAlign.Center)
|
|
|
|
|
|
|
|
|
|
|
|
this.field('从', 'YYYY-MM-DD(可空)', this.sFrom, (v: string): void => {
|
|
|
|
|
|
this.sFrom = v;
|
|
|
|
|
|
})
|
|
|
|
|
|
this.field('到', 'YYYY-MM-DD(可空)', this.sTo, (v: string): void => {
|
|
|
|
|
|
this.sTo = v;
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
Row({ space: 8 }) {
|
|
|
|
|
|
Text(this.searchLoading ? '搜索中…' : '搜索')
|
|
|
|
|
|
.fontSize(FbMetric.fontSmall)
|
|
|
|
|
|
.fontColor(Color.White)
|
|
|
|
|
|
.textAlign(TextAlign.Center)
|
|
|
|
|
|
.layoutWeight(1)
|
|
|
|
|
|
.height(40)
|
|
|
|
|
|
.backgroundColor(this.searchLoading
|
|
|
|
|
|
? fbAlpha(this.accentColor(), 0.5) : this.accentColor())
|
|
|
|
|
|
.borderRadius(FbMetric.radiusS)
|
|
|
|
|
|
.onClick((): void => {
|
|
|
|
|
|
this.doSearch();
|
|
|
|
|
|
})
|
|
|
|
|
|
Text('清空')
|
|
|
|
|
|
.fontSize(FbMetric.fontSmall)
|
|
|
|
|
|
.fontColor(FbColors.muted)
|
|
|
|
|
|
.textAlign(TextAlign.Center)
|
|
|
|
|
|
.layoutWeight(1)
|
|
|
|
|
|
.height(40)
|
|
|
|
|
|
.backgroundColor(FbColors.inputBg)
|
|
|
|
|
|
.borderRadius(FbMetric.radiusS)
|
|
|
|
|
|
.onClick((): void => {
|
|
|
|
|
|
this.sTitle = '';
|
|
|
|
|
|
this.sCinemaId = 0;
|
|
|
|
|
|
this.sFrom = '';
|
|
|
|
|
|
this.sTo = '';
|
|
|
|
|
|
this.searchRows = [];
|
|
|
|
|
|
this.searchError = '';
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.margin({ top: 2 })
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding({ top: 12 })
|
|
|
|
|
|
.alignItems(HorizontalAlign.Start)
|
|
|
|
|
|
|
|
|
|
|
|
// ---- 结果 ----
|
|
|
|
|
|
if (this.searchError !== '') {
|
|
|
|
|
|
Text(this.searchError)
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(FbColors.faint)
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding({ top: 12, bottom: 8 })
|
|
|
|
|
|
}
|
|
|
|
|
|
if (this.searchRows.length > 0) {
|
|
|
|
|
|
this.sectionTitle('命中 ' + this.searchRows.length.toString() + ' 条')
|
|
|
|
|
|
Column({ space: 0 }) {
|
|
|
|
|
|
ForEach(this.searchRows, (r: FbApiMovieSearchRow) => {
|
|
|
|
|
|
Column({ space: 0 }) {
|
|
|
|
|
|
Row({ space: 10 }) {
|
|
|
|
|
|
Column()
|
|
|
|
|
|
.width(4)
|
|
|
|
|
|
.height(34)
|
|
|
|
|
|
.backgroundColor(r.cinema_color !== null && r.cinema_color !== undefined
|
|
|
|
|
|
&& r.cinema_color !== '' ? r.cinema_color : this.accentColor())
|
|
|
|
|
|
.borderRadius(2)
|
|
|
|
|
|
Column({ space: 3 }) {
|
|
|
|
|
|
Text(r.title !== undefined && r.title !== '' ? r.title : '未命名影片')
|
|
|
|
|
|
.fontSize(FbMetric.fontSmall)
|
|
|
|
|
|
.fontColor(FbColors.ink)
|
|
|
|
|
|
.maxLines(1)
|
|
|
|
|
|
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
|
|
|
|
|
Row({ space: 6 }) {
|
|
|
|
|
|
Text(this.dateOf(r.starts_at))
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(FbColors.muted)
|
|
|
|
|
|
if (r.cinema_name !== null && r.cinema_name !== undefined && r.cinema_name !== '') {
|
|
|
|
|
|
Text(r.cinema_name)
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(FbColors.faint)
|
|
|
|
|
|
.maxLines(1)
|
|
|
|
|
|
.layoutWeight(1)
|
|
|
|
|
|
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
}
|
|
|
|
|
|
.alignItems(HorizontalAlign.Start)
|
|
|
|
|
|
.layoutWeight(1)
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding({ top: 10, bottom: 10 })
|
|
|
|
|
|
.alignItems(VerticalAlign.Center)
|
|
|
|
|
|
|
|
|
|
|
|
Divider().strokeWidth(0.5).color(FbColors.line)
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
}, (r: FbApiMovieSearchRow): string => 'srch_' + (r.id !== undefined ? r.id : 0).toString())
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** `2026-10-19 19:30:00` → `2026-10-19` */
|
|
|
|
|
|
private dateOf(v: string | undefined | null): string {
|
|
|
|
|
|
if (v === undefined || v === null || v.length < 10) {
|
|
|
|
|
|
return '';
|
|
|
|
|
|
}
|
|
|
|
|
|
return v.substring(0, 10);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== ④ 年度报告 ====================
|
|
|
|
|
|
|
|
|
|
|
|
@Builder
|
|
|
|
|
|
reportPane() {
|
|
|
|
|
|
if (this.reportLoading) {
|
|
|
|
|
|
this.loadingRow('正在生成年度报告…')
|
|
|
|
|
|
} else if (this.reportError !== '') {
|
|
|
|
|
|
this.errorRow(this.reportError, (): void => {
|
|
|
|
|
|
this.loadReport(this.reportYear);
|
|
|
|
|
|
})
|
|
|
|
|
|
} else if (this.report !== null) {
|
|
|
|
|
|
// ---- 年份切换 ----
|
|
|
|
|
|
if (this.reportYears().length > 1) {
|
|
|
|
|
|
Scroll() {
|
|
|
|
|
|
Row({ space: 6 }) {
|
|
|
|
|
|
ForEach(this.reportYears(), (y: number) => {
|
|
|
|
|
|
Text(y.toString() + ' 年')
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(this.reportYear === y ? Color.White : FbColors.muted)
|
|
|
|
|
|
.padding({ left: 12, right: 12, top: 5, bottom: 5 })
|
|
|
|
|
|
.backgroundColor(this.reportYear === y ? this.accentColor() : FbColors.inputBg)
|
|
|
|
|
|
.borderRadius(FbMetric.radiusPill)
|
|
|
|
|
|
.onClick((): void => {
|
|
|
|
|
|
this.loadReport(y);
|
|
|
|
|
|
})
|
|
|
|
|
|
}, (y: number): string => 'ry_' + y.toString())
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
.scrollable(ScrollDirection.Horizontal)
|
|
|
|
|
|
.scrollBar(BarState.Off)
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding({ top: 10, bottom: 2 })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---- 大数 ----
|
|
|
|
|
|
Row({ space: 0 }) {
|
|
|
|
|
|
this.statCell('看了', this.numField('total'))
|
|
|
|
|
|
this.statCell('观影天数', this.numField('days'))
|
|
|
|
|
|
this.statCell('平均每月', this.numField('avg_per_month'))
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding({ top: 12, bottom: 10 })
|
|
|
|
|
|
|
|
|
|
|
|
// ---- 几条"最" ----
|
|
|
|
|
|
this.reportLine('最常去的影院', this.strField('top_cinema'),
|
|
|
|
|
|
this.numField('top_cinema_count'), '次')
|
|
|
|
|
|
this.reportLine('一起看最多的人', this.strField('top_person'),
|
|
|
|
|
|
this.numField('top_person_count'), '场')
|
|
|
|
|
|
this.reportLine('最多人一起看的', this.strField('most_viewers_movie'),
|
|
|
|
|
|
this.numField('most_viewers_count'), '人')
|
|
|
|
|
|
|
|
|
|
|
|
// ---- 首末场 ----
|
|
|
|
|
|
/* ⚠️ `@Builder` 里**不能写 `const` 声明**(编译器报 "Only UI component
|
|
|
|
|
|
syntax can be written here")—— 只能直接调方法。
|
|
|
|
|
|
⇒ 判空/拼接都收进 `reportSpan()` 里,返回空串就不渲染这行。 */
|
|
|
|
|
|
if (this.reportSpan() !== '') {
|
|
|
|
|
|
this.sectionTitle('这一年')
|
|
|
|
|
|
Text(this.reportSpan())
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(FbColors.muted)
|
|
|
|
|
|
.padding({ top: 2 })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---- 月份分布 ----
|
|
|
|
|
|
if (this.monthDist().length > 0) {
|
|
|
|
|
|
this.sectionTitle('每月分布')
|
|
|
|
|
|
Column({ space: 6 }) {
|
|
|
|
|
|
ForEach(this.monthDist(), (m: FbMovieMonthStat) => {
|
|
|
|
|
|
this.barRow(
|
|
|
|
|
|
m.label !== undefined ? m.label : '',
|
|
|
|
|
|
m.cnt !== undefined ? m.cnt : 0,
|
|
|
|
|
|
this.maxMonthDist(),
|
|
|
|
|
|
this.accentColor())
|
|
|
|
|
|
}, (m: FbMovieMonthStat): string => 'rd_' + (m.label !== undefined ? m.label : ''))
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding({ top: 4 })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (this.numField('total') === 0) {
|
|
|
|
|
|
Text('这一年(' + this.reportYear.toString() + ')没有你参与的观影记录。')
|
|
|
|
|
|
.fontSize(FbMetric.fontSmall)
|
|
|
|
|
|
.fontColor(FbColors.faint)
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding({ top: 24, bottom: 16 })
|
|
|
|
|
|
.textAlign(TextAlign.Center)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private reportYears(): number[] {
|
|
|
|
|
|
if (this.report === null || this.report.years === undefined) {
|
|
|
|
|
|
return [];
|
|
|
|
|
|
}
|
|
|
|
|
|
return this.report.years;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private rp(): Object {
|
|
|
|
|
|
if (this.report === null || this.report.stats === undefined) {
|
|
|
|
|
|
// ⚠️ ArkTS 不允许空对象字面量 ⇒ 用一个显式声明的空 interface
|
|
|
|
|
|
return EMPTY_STATS;
|
|
|
|
|
|
}
|
|
|
|
|
|
return this.report.stats as Object;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 年报告里的「第一场 … 最后一场 …」;没有首场时返回空串(不渲染) */
|
|
|
|
|
|
private reportSpan(): string {
|
|
|
|
|
|
const f: string = this.strField('first_date');
|
|
|
|
|
|
if (f === '') {
|
|
|
|
|
|
return '';
|
|
|
|
|
|
}
|
|
|
|
|
|
const l: string = this.strField('last_date');
|
|
|
|
|
|
return '第一场 ' + f + ' · 最后一场 ' + (l !== '' ? l : '—');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 取一个数值字段(JSON 解析出来的是弱类型,统一在这里兜底) */
|
|
|
|
|
|
private numField(key: string): number {
|
|
|
|
|
|
const o: Record<string, Object> = this.rp() as Record<string, Object>;
|
|
|
|
|
|
const v: Object | undefined = o[key];
|
|
|
|
|
|
return typeof v === 'number' ? v : 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private strField(key: string): string {
|
|
|
|
|
|
const o: Record<string, Object> = this.rp() as Record<string, Object>;
|
|
|
|
|
|
const v: Object | undefined = o[key];
|
|
|
|
|
|
return typeof v === 'string' ? v : '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 月份分布 → 条形数据(1..12 月) */
|
|
|
|
|
|
private monthDist(): FbMovieMonthStat[] {
|
|
|
|
|
|
const o: Record<string, Object> = this.rp() as Record<string, Object>;
|
|
|
|
|
|
const md: Object | undefined = o['month_distribution'];
|
|
|
|
|
|
const out: FbMovieMonthStat[] = [];
|
|
|
|
|
|
if (md === undefined || md === null) {
|
|
|
|
|
|
return out;
|
|
|
|
|
|
}
|
|
|
|
|
|
const m: Record<string, Object> = md as Record<string, Object>;
|
|
|
|
|
|
for (let i = 1; i <= 12; i++) {
|
|
|
|
|
|
const v: Object | undefined = m[i.toString()];
|
|
|
|
|
|
const it: FbMovieMonthStat = {};
|
|
|
|
|
|
it.label = i.toString() + ' 月';
|
|
|
|
|
|
it.cnt = typeof v === 'number' ? v : 0;
|
|
|
|
|
|
out.push(it);
|
|
|
|
|
|
}
|
|
|
|
|
|
return out;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private maxMonthDist(): number {
|
|
|
|
|
|
let m: number = 1;
|
|
|
|
|
|
for (const it of this.monthDist()) {
|
|
|
|
|
|
const n: number = it.cnt !== undefined ? it.cnt : 0;
|
|
|
|
|
|
if (n > m) {
|
|
|
|
|
|
m = n;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return m;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 报告里的一条「最 XX」。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ `total` 字段在端上是**弱类型**(服务端可能给 int),
|
|
|
|
|
|
* 所以统一走 `numField/strField` 兜底,不直接下标取值。
|
|
|
|
|
|
*/
|
|
|
|
|
|
@Builder
|
|
|
|
|
|
reportLine(label: string, value: string, count: number, unit: string) {
|
|
|
|
|
|
if (value !== '' && value !== '无') {
|
|
|
|
|
|
Row({ space: 8 }) {
|
|
|
|
|
|
Text(label)
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(FbColors.faint)
|
|
|
|
|
|
.width(96)
|
|
|
|
|
|
Text(value)
|
|
|
|
|
|
.fontSize(FbMetric.fontSmall)
|
|
|
|
|
|
.fontColor(FbColors.ink)
|
|
|
|
|
|
.maxLines(1)
|
|
|
|
|
|
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
|
|
|
|
|
.layoutWeight(1)
|
|
|
|
|
|
if (count > 0) {
|
|
|
|
|
|
Text(count.toString() + ' ' + unit)
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(this.accentColor())
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding({ top: 6, bottom: 6 })
|
|
|
|
|
|
.alignItems(VerticalAlign.Center)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 小组件 ====================
|
|
|
|
|
|
|
|
|
|
|
|
@Builder
|
|
|
|
|
|
sectionTitle(t: string) {
|
|
|
|
|
|
Text(t)
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(FbColors.faint)
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding({ top: 14, bottom: 2 })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Builder
|
|
|
|
|
|
loadingRow(t: string) {
|
|
|
|
|
|
Row({ space: 8 }) {
|
|
|
|
|
|
LoadingProgress().width(18).height(18).color(FbColors.faint)
|
|
|
|
|
|
Text(t)
|
|
|
|
|
|
.fontSize(FbMetric.fontSmall)
|
|
|
|
|
|
.fontColor(FbColors.faint)
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.justifyContent(FlexAlign.Center)
|
|
|
|
|
|
.padding({ top: 40, bottom: 40 })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 失败 + 一个「重试」(用户最烦"点了没反应",给一条出路) */
|
|
|
|
|
|
@Builder
|
|
|
|
|
|
errorRow(msg: string, retry: () => void) {
|
|
|
|
|
|
Column({ space: 10 }) {
|
|
|
|
|
|
Text(msg)
|
|
|
|
|
|
.fontSize(FbMetric.fontSmall)
|
|
|
|
|
|
.fontColor(FbColors.faint)
|
|
|
|
|
|
.textAlign(TextAlign.Center)
|
|
|
|
|
|
Text('重试')
|
|
|
|
|
|
.fontSize(FbMetric.fontSmall)
|
|
|
|
|
|
.fontColor(this.accentColor())
|
|
|
|
|
|
.height(34)
|
|
|
|
|
|
.padding({ left: 16, right: 16 })
|
|
|
|
|
|
.borderRadius(FbMetric.radiusS)
|
|
|
|
|
|
.backgroundColor(fbAlpha(this.accentColor(), 0.10))
|
|
|
|
|
|
.textAlign(TextAlign.Center)
|
|
|
|
|
|
.onClick((): void => {
|
|
|
|
|
|
retry();
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding({ top: 32, bottom: 32 })
|
|
|
|
|
|
.alignItems(HorizontalAlign.Center)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 新建影院时可选的颜色(与网页 `Cinema::PALETTE` 同一族,取前若干个常用的)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 不是**全部**调色板(那有 40 个,竖排一片看不出差别)——
|
|
|
|
|
|
* 这里挑的是"色相差得开"的那几个,用户一眼能分。
|
|
|
|
|
|
*/
|
|
|
|
|
|
const PALETTE: string[] = [
|
|
|
|
|
|
'#7A4E8C', '#e84393', '#e91e63', '#f06292',
|
|
|
|
|
|
'#2980b9', '#6c5ce7', '#16a085', '#2ecc71',
|
|
|
|
|
|
'#f8c471', '#a67c52', '#95a5a6', '#7f8c8d'
|
|
|
|
|
|
];
|