2026-10-11 22:07:48 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 家书 FamilyBook · 观影模块(鸿蒙端)
|
|
|
|
|
|
*
|
|
|
|
|
|
* 对应 Web 端 `templates/movies/index.php`。Web 端是**海报墙**(waterfall 瀑布流),
|
|
|
|
|
|
* 端上改成**两列网格**:手机宽度下两列海报刚好一屏看清 4–6 部,
|
|
|
|
|
|
* 比瀑布流(列宽随图片高度浮动)更整齐、也更好点。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 页头右侧两个入口:搜索、设置(影院管理 / 统计 / 年度报告 —— 与 Web 端一致,
|
|
|
|
|
|
* 「影院」不单独占抽屉位置,它是观影模块的一部分)。
|
2026-10-18 03:35:39 +08:00
|
|
|
|
*
|
|
|
|
|
|
* 🔴 **本地优先(local-first)的第一个落地模块**(用户 2026-10-12 定):
|
|
|
|
|
|
* 数据流是「接口 → 本地库 → UI」,而不是「UI 直接读接口」。
|
|
|
|
|
|
* · `aboutToAppear` 时先读**本地库**渲染(有数据就立刻显示,不等网络);
|
|
|
|
|
|
* · 然后**后台**跑一次 `sync()` 把服务端数据补进来;
|
|
|
|
|
|
* · 同步完再读一次本地库刷新界面。
|
|
|
|
|
|
* 这样断网时页面照常可用(因为读的本来就是本地),
|
|
|
|
|
|
* 有网时数据会自动变新。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 为什么不在 `aboutToAppear` 里 `await sync()` 再渲染:
|
|
|
|
|
|
* 那样就成了"网络优先"——转圈等 2 秒才出内容,
|
|
|
|
|
|
* 断网时更是完全空白。**先渲染本地、再后台同步**是这套架构的要点。
|
2026-10-11 22:07:48 +08:00
|
|
|
|
*/
|
|
|
|
|
|
|
2026-10-18 23:12:57 +08:00
|
|
|
|
import { FbColors, FbMetric, fbPalette, fbTint, FbTint, FB_MASK } from '../../common/Theme';
|
2026-10-11 22:07:48 +08:00
|
|
|
|
import { FbAppState, fbAppState } from '../../common/Store';
|
2026-10-18 03:35:39 +08:00
|
|
|
|
import { FbKinChip } from '../../model/Types';
|
|
|
|
|
|
import { FbCinemaItem } from '../../data/FbMovieRepo';
|
2026-10-11 22:07:48 +08:00
|
|
|
|
import { FbDot, FbEmptyBox, FbIconButton, FbPersonChip, FbPrimaryButton, FbRating, FbSectionTitle } from '../../components/Widgets';
|
2026-10-12 12:06:00 +08:00
|
|
|
|
import { FB_DOCK_RESERVE } from '../../components/FloatDock';
|
2026-10-18 03:35:39 +08:00
|
|
|
|
import { FbDb } from '../../data/FbDb';
|
|
|
|
|
|
import { FbMovieDetail, FbMovieListItem, FbMovieRepo, FbSyncResult } from '../../data/FbMovieRepo';
|
|
|
|
|
|
import { FbMovieDetailSheet } from '../../components/FbMovieDetailSheet';
|
|
|
|
|
|
import { FbCinemaSheet } from '../../components/FbCinemaSheet';
|
|
|
|
|
|
import { FbModuleSettingsSheet } from '../../components/FbModuleSettingsSheet';
|
2026-10-18 23:12:57 +08:00
|
|
|
|
import { FB_BACK } from '../../common/FbBack';
|
2026-10-18 03:35:39 +08:00
|
|
|
|
import { FB_SET_MOVIES } from '../../model/FbSettingsMenus';
|
|
|
|
|
|
import { FB_TOKEN_STORE } from '../../data/FbTokenStore';
|
|
|
|
|
|
import { hilog } from '@kit.PerformanceAnalysisKit';
|
|
|
|
|
|
import { common } from '@kit.AbilityKit';
|
|
|
|
|
|
import { BusinessError } from '@kit.BasicServicesKit';
|
|
|
|
|
|
import { fbOnScroll, fbOnTouch } from '../../common/FbScroll';
|
2026-10-18 16:01:41 +08:00
|
|
|
|
import { fbImageSrc } from '../../data/FbAttachQueue';
|
2026-10-18 03:35:39 +08:00
|
|
|
|
|
|
|
|
|
|
const DOMAIN: number = 0x0000;
|
|
|
|
|
|
const TAG: string = 'FbMoviesPage';
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 把接口的完整时间('YYYY-MM-DD HH:mm:ss')裁成卡片上够用的短形式。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 只在**显示**时裁,存库仍存完整值 —— 否则详情页/排序会丢精度。
|
|
|
|
|
|
* ⚠️ 输入可能为空(老数据没填时间)⇒ 必须返回空串而不是 'undefined'。
|
|
|
|
|
|
*/
|
|
|
|
|
|
function shortTime(v: string): string {
|
|
|
|
|
|
if (v.length < 16) {
|
|
|
|
|
|
return v; // 短于 'YYYY-MM-DD HH:mm' 就原样返回(含空串)
|
|
|
|
|
|
}
|
|
|
|
|
|
return v.substring(0, 16); // 去掉最后的 ':ss'
|
|
|
|
|
|
}
|
2026-10-11 22:07:48 +08:00
|
|
|
|
|
|
|
|
|
|
@ComponentV2
|
|
|
|
|
|
export struct FbMoviesPage {
|
|
|
|
|
|
@Param app: FbAppState = fbAppState();
|
2026-10-12 10:06:55 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 顶栏插槽:由 Index.ets 传入,放在本页滚动流最前面 ⇒ 随页面一起滚走。
|
2026-10-18 03:35:39 +08:00
|
|
|
|
* 🔴 用户要求「向上滑动时滑动时标题栏跟着一起滑走」。
|
2026-10-12 10:06:55 +08:00
|
|
|
|
*/
|
|
|
|
|
|
@BuilderParam topBar: () => void;
|
2026-10-11 22:07:48 +08:00
|
|
|
|
|
2026-10-18 03:35:39 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 观影列表(**来自本地库**)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 初值给空数组而不是演示数据:一上来就显示假数据、随后被真数据替换,
|
|
|
|
|
|
* 用户会看到"闪一下"。空数组 + 一个明确的"加载中/暂无"状态更诚实。
|
|
|
|
|
|
* (演示数据仍保留在 DemoData 里,供后续做空态预览用。)
|
|
|
|
|
|
*/
|
|
|
|
|
|
@Local movies: FbMovieListItem[] = [];
|
|
|
|
|
|
/** 是否正在首次加载(决定显示"加载中"还是"暂无记录") */
|
|
|
|
|
|
@Local loading: boolean = true;
|
|
|
|
|
|
/** 观影页顶部一行提示(自检/同步状态共用;谁最后写谁显示) */
|
|
|
|
|
|
@Local syncNote: string = '';
|
|
|
|
|
|
/** 临时自检结果(与同步提示分开,避免互相覆盖) */
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 海报加载失败的电影 id。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 🔴 为什么需要它:海报是网络图,取不到是**常态**(没网、图被删、令牌失效)。
|
|
|
|
|
|
* 不处理的话这张卡会一直显示"半张白图",用户以为页面坏了。
|
|
|
|
|
|
* ⇒ 记下失败的 id,那张卡改回"图标 + 片名"的占位(与"本来就没海报"同观感)。
|
|
|
|
|
|
* ⚠️ 用 `@Local` + 数组整体替换来触发刷新:
|
|
|
|
|
|
* ArkUI 的 `@Local` 只代理第一层,直接 `push` 到数组里**不会刷新界面**
|
|
|
|
|
|
* (§05-state 的规则)。所以每次都用新数组赋值。
|
|
|
|
|
|
*/
|
|
|
|
|
|
@Local posterFailIds: number[] = [];
|
|
|
|
|
|
|
|
|
|
|
|
// ---- 详情浮层 ----
|
|
|
|
|
|
/** 浮层是否显示 */
|
|
|
|
|
|
@Local sheetOpen: boolean = false;
|
|
|
|
|
|
/** 浮层里的详情(null = 还没加载出来) */
|
|
|
|
|
|
@Local sheetDetail: FbMovieDetail | null = null;
|
|
|
|
|
|
/** 详情是否加载中 */
|
|
|
|
|
|
@Local sheetLoading: boolean = false;
|
|
|
|
|
|
/** 详情加载失败信息(空串 = 没失败) */
|
|
|
|
|
|
@Local sheetError: string = '';
|
|
|
|
|
|
/** 浮层对应的列表项评分(详情接口不给评分,从列表带,见 FbMovieRepo.detail 说明) */
|
|
|
|
|
|
@Local sheetRating: number = 0;
|
|
|
|
|
|
@Local sheetReviewCount: number = 0;
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 浮层正在展示的电影 id。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 两个用途,缺一不可:
|
|
|
|
|
|
* ① **防重入**:连点同一条时不重复叠浮层(实测会叠层,照片数翻倍);
|
|
|
|
|
|
* ② **防串数据**:详情是异步拉的,回来时要确认"还是这条"才写入
|
|
|
|
|
|
* (否则点 A 又点 B,A 的数据会糊到 B 的浮层上)。
|
|
|
|
|
|
*/
|
|
|
|
|
|
@Local sheetMovieId: number = 0;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 影院列表(**从本地库读**,`/api/v1/cinemas` 的同步结果)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 🔴 之前这里是 `FB_DEMO_CINEMAS`(写死的演示数组),只用来显示"影院 N 家"
|
|
|
|
|
|
* —— 而「5 家」那个数字**恰好**跟真实值一样,所以看不出是假的。
|
|
|
|
|
|
* 改成真实列表后,数字与列表同源,不会再对不上。
|
|
|
|
|
|
*/
|
|
|
|
|
|
@Local cinemas: FbCinemaItem[] = [];
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 模块设置浮层是否打开。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 🔴 用户 2026-10-16:右上角按钮统一改成网页版那种**三条横线**(☰),
|
|
|
|
|
|
* 点开是**这个模块的设置菜单**(照 `templates/movies/_settings_menu.php`)。
|
|
|
|
|
|
*/
|
|
|
|
|
|
@Local settingsOpen: boolean = false;
|
|
|
|
|
|
/** 影院浮层是否打开 */
|
|
|
|
|
|
@Local cinemaSheetOpen: boolean = false;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 🔴 浮层开合时**让底部悬浮栏让位**(与名册页同一处 bug,见 `FbPeoplePage` 的说明)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 浮层装在**页面自己的 Stack** 里,而悬浮栏在**更外层 `Index` 的 Stack** 里
|
|
|
|
|
|
* ⇒ 悬浮栏层级更高 ⇒ 浮层底部的按钮会与它重叠、点击被抢走。
|
|
|
|
|
|
* ⇒ 用 `@Monitor` 一处声明、自动跟随派生(避免在多个赋值点手写、漏一处就出事)。
|
|
|
|
|
|
*/
|
|
|
|
|
|
@Monitor('sheetOpen', 'cinemaSheetOpen', 'settingsOpen')
|
|
|
|
|
|
onSheetChanged(): void {
|
|
|
|
|
|
const app: FbAppState = fbAppState();
|
|
|
|
|
|
app.sheetCount = (this.sheetOpen || this.cinemaSheetOpen || this.settingsOpen) ? 1 : 0;
|
|
|
|
|
|
}
|
2026-10-11 22:07:48 +08:00
|
|
|
|
/** 模块主题色(观影 = 紫) */
|
|
|
|
|
|
private accent: string = '#7A4E8C';
|
2026-10-18 03:35:39 +08:00
|
|
|
|
private repo: FbMovieRepo = FbMovieRepo.inst();
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 页面出现:先读本地库渲染,再后台同步。
|
|
|
|
|
|
* ⚠️ 不能声明成 async 让框架 await —— 生命周期钩子返回 void 即可,
|
|
|
|
|
|
* 异步逻辑在内部自己跑(否则会影响页面首帧时机)。
|
|
|
|
|
|
*/
|
2026-10-18 23:12:57 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 🔴 v59e(用户 2026-10-18):系统返回手势 —— 只关**最上面一层**浮层。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 本页三个浮层:详情(`sheetOpen`)/ 影院(`cinemaSheetOpen`)/ 设置(`settingsOpen`)。
|
|
|
|
|
|
* ⚠️ 顺序按"后出现的在上层":设置里点「影院管理」会把设置关掉、开影院
|
|
|
|
|
|
* (两者不会同时开),但反过来"影院开着时又点设置"是可能的 ⇒ 先收设置。
|
|
|
|
|
|
*/
|
|
|
|
|
|
aboutToDisappear(): void {
|
|
|
|
|
|
FB_BACK.unregister('movies');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private backOnce(): boolean {
|
|
|
|
|
|
if (this.settingsOpen) {
|
|
|
|
|
|
this.settingsOpen = false;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (this.cinemaSheetOpen) {
|
|
|
|
|
|
this.cinemaSheetOpen = false;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (this.sheetOpen) {
|
|
|
|
|
|
this.sheetOpen = false;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-18 03:35:39 +08:00
|
|
|
|
aboutToAppear(): void {
|
2026-10-18 23:12:57 +08:00
|
|
|
|
FB_BACK.register('movies', (): boolean => this.backOnce());
|
2026-10-18 03:35:39 +08:00
|
|
|
|
this.bootstrap();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 该电影的海报是否已确认加载失败 */
|
|
|
|
|
|
private posterFailed(id: number): boolean {
|
|
|
|
|
|
return this.posterFailIds.indexOf(id) >= 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 记下"这张海报加载失败"。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 必须**换成新数组**赋值(`concat`),不能 `push` ——
|
|
|
|
|
|
* `@Local` 不代理数组内部变化,push 进去界面不会刷新
|
|
|
|
|
|
* (表现为"图片还是白的,占位也没出现",看着像没生效)。
|
|
|
|
|
|
* ⚠️ 已经记过就不重复加:`onError` 可能被多次触发,
|
|
|
|
|
|
* 重复添加会让数组无限增长(虽然去重判断也要 O(n),但这里 n 很小)。
|
|
|
|
|
|
*/
|
|
|
|
|
|
private markPosterFailed(id: number): void {
|
|
|
|
|
|
if (this.posterFailed(id)) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.posterFailIds = this.posterFailIds.concat([id]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 点卡片 → 打开详情浮层。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 🔴 先"立刻开浮层 + 显示加载中",再异步拉详情:
|
|
|
|
|
|
* 如果等详情回来才开浮层,用户点下去会有**明显延迟**(网络快则一闪、
|
|
|
|
|
|
* 慢则好几秒毫无反应,用户以为没点到)。
|
|
|
|
|
|
* 先把浮层弹出来、里面写"正在加载详情…",体感完全不同。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 未登录(没令牌)时**不开浮层**,而是给一句提示:
|
|
|
|
|
|
* 详情要连服务端拉,没令牌必然失败;让用户看到"加载失败"不如
|
|
|
|
|
|
* 直接说清"没连服务器"。
|
|
|
|
|
|
*/
|
|
|
|
|
|
private openDetail(mv: FbMovieListItem): void {
|
|
|
|
|
|
const token: string = FB_TOKEN_STORE.get();
|
|
|
|
|
|
if (token === '') {
|
|
|
|
|
|
this.syncNote = '未连接服务器,详情需要联网查看';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 🔴 **防重入**:已经是这条、且浮层已经开着 ⇒ 直接返回。
|
|
|
|
|
|
// 不加这个判断时,连点卡片会**反复叠加浮层**
|
|
|
|
|
|
// (2026-10-12 实测:浮层里的照片数从 5 变成 9,就是叠了两层)。
|
|
|
|
|
|
if (this.sheetOpen && this.sheetMovieId === mv.id) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.sheetMovieId = mv.id;
|
|
|
|
|
|
// 先把浮层弹出来(带加载态),再拉数据
|
|
|
|
|
|
this.sheetRating = mv.reviewAvg;
|
|
|
|
|
|
this.sheetReviewCount = mv.reviewCount;
|
|
|
|
|
|
this.sheetDetail = null;
|
|
|
|
|
|
this.sheetError = '';
|
|
|
|
|
|
this.sheetLoading = true;
|
|
|
|
|
|
this.sheetOpen = true;
|
|
|
|
|
|
this.loadDetail(mv.id, token, mv);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 拉详情并填进浮层 */
|
|
|
|
|
|
private async loadDetail(id: number, token: string, mv: FbMovieListItem): Promise<void> {
|
|
|
|
|
|
const d: FbMovieDetail = await this.repo.detail(id, token, mv);
|
|
|
|
|
|
// ⚠️ 用户可能在等待期间**已经关掉浮层**(或点了别的卡片)
|
|
|
|
|
|
// ⇒ 回来时先确认浮层还开着、且**还是这一条**,否则会把旧数据糊到新浮层上。
|
|
|
|
|
|
if (!this.sheetOpen || this.sheetMovieId !== id) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.sheetLoading = false;
|
|
|
|
|
|
if (!d.ok) {
|
|
|
|
|
|
this.sheetError = d.error;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.sheetDetail = d;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 启动流程:开库 → 读本地 → 后台同步 → 再读本地 */
|
|
|
|
|
|
private async bootstrap(): Promise<void> {
|
|
|
|
|
|
hilog.info(DOMAIN, TAG, 'bootstrap 开始');
|
|
|
|
|
|
try {
|
|
|
|
|
|
// ① 取 Ability 上下文。
|
|
|
|
|
|
// 🔴 用 `getUIContext().getHostContext()`,**不用全局 `getContext(this)`**:
|
|
|
|
|
|
// 全局 getContext 在 Stage 模型多 ArkUI 实例下可能取错/取不到,
|
|
|
|
|
|
// 规则文件明确要求换掉(r/quick-rules/02-uicontext.md)。
|
|
|
|
|
|
// RDB / preferences 都要靠这个 Context 定位应用沙箱目录。
|
|
|
|
|
|
const ctx: common.Context | undefined = this.getUIContext().getHostContext();
|
|
|
|
|
|
if (ctx === undefined) {
|
|
|
|
|
|
hilog.error(DOMAIN, TAG, '拿不到 Ability Context,本地库无法打开');
|
|
|
|
|
|
this.loading = false;
|
|
|
|
|
|
this.syncNote = '本地存储不可用(上下文获取失败)';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
hilog.info(DOMAIN, TAG, '拿到 Context,准备打开本地库');
|
|
|
|
|
|
// ② 打开本地库(首装会建表;这一步本身很快)
|
|
|
|
|
|
await FbDb.inst().open(ctx);
|
|
|
|
|
|
hilog.info(DOMAIN, TAG, '本地库打开成功');
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
// ⚠️ 把**真实错误**显示到界面上:
|
|
|
|
|
|
// 这台设备的 hilog 缓冲区读不出内容(已确认),界面是唯一可靠通道。
|
|
|
|
|
|
// 临时诊断用,定位完就换回友好文案。
|
|
|
|
|
|
const e: BusinessError = err as BusinessError;
|
|
|
|
|
|
const detail: string = (e.code !== undefined ? 'code=' + e.code.toString() + ' ' : '') +
|
|
|
|
|
|
(e.message !== undefined ? e.message : JSON.stringify(err));
|
|
|
|
|
|
hilog.error(DOMAIN, TAG, '本地库打开失败:%{public}s', detail);
|
|
|
|
|
|
this.loading = false;
|
|
|
|
|
|
this.syncNote = '本地库打开失败:' + detail;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ③ 先读本地(有数据立刻显示)
|
|
|
|
|
|
await this.reloadFromLocal();
|
|
|
|
|
|
|
|
|
|
|
|
// ④ 后台同步(不阻塞界面)
|
|
|
|
|
|
this.syncInBackground();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 临时:把本地库自检结果拼成一行显示(定位完删除) */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** 从本地库读列表刷新界面 */
|
|
|
|
|
|
private async reloadFromLocal(): Promise<void> {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const rows: FbMovieListItem[] = await this.repo.list(300);
|
|
|
|
|
|
this.movies = rows;
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
hilog.error(DOMAIN, TAG, '读本地观影失败:%{public}s', JSON.stringify(err));
|
|
|
|
|
|
}
|
|
|
|
|
|
// 🔴 影院与观影列表**分开 try**。
|
|
|
|
|
|
// 为什么:影院是"附属信息"(只是页头那行"影院 N 家"),
|
|
|
|
|
|
// 它读失败**不该**影响观影列表的显示。
|
|
|
|
|
|
// ⚠️ 踩过的坑:一开始把这段塞进了上面的 `catch` 里
|
|
|
|
|
|
// (脚本按"插在 finally 前"定位,结果落进了 catch)——
|
|
|
|
|
|
// 表现为"只有读失败时才有影院",而正常路径永远是 0 家。
|
|
|
|
|
|
// ⇒ **自动插入代码后必须看插入点的上下文**,别只看"插进去了"。
|
|
|
|
|
|
try {
|
|
|
|
|
|
this.cinemas = await this.repo.cinemaList();
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
hilog.warn(DOMAIN, TAG, '读本地影院失败:%{public}s', JSON.stringify(err));
|
|
|
|
|
|
}
|
|
|
|
|
|
this.loading = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 后台同步:拉接口 → 落本地库 → 再读一次刷新界面。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 失败**不弹窗、不清空已有数据** —— 本地优先下"同步失败"只是
|
|
|
|
|
|
* "这次没更新",用户手上的数据仍然可用。只在页面上留一行小字说明。
|
|
|
|
|
|
*/
|
|
|
|
|
|
private async syncInBackground(): Promise<void> {
|
|
|
|
|
|
const token: string = FB_TOKEN_STORE.get();
|
|
|
|
|
|
if (token === '') {
|
|
|
|
|
|
// 还没有令牌:不做同步,但**本地数据照样能看**(这就是落盘的价值)
|
|
|
|
|
|
this.syncNote = '未登录,显示的是本地数据';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const res: FbSyncResult = await this.repo.sync(token);
|
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
|
hilog.warn(DOMAIN, TAG, '观影同步失败:%{public}s', res.error);
|
|
|
|
|
|
this.syncNote = '同步失败:' + res.error;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.syncNote = '';
|
|
|
|
|
|
await this.reloadFromLocal(); // 同步成功再刷一次,把新数据显示出来
|
|
|
|
|
|
}
|
2026-10-11 22:07:48 +08:00
|
|
|
|
|
|
|
|
|
|
build() {
|
2026-10-18 03:35:39 +08:00
|
|
|
|
// 🔴 用 Stack 把"页面内容"和"详情浮层"叠起来:
|
|
|
|
|
|
// 浮层是页面内的一个覆盖层(**不是系统弹窗**,理由见
|
|
|
|
|
|
// FbMovieDetailSheet 的文件头说明)。
|
|
|
|
|
|
Stack({ alignContent: Alignment.Bottom }) {
|
|
|
|
|
|
this.pageContent()
|
|
|
|
|
|
|
|
|
|
|
|
// ---- 详情浮层 ----
|
|
|
|
|
|
if (this.sheetOpen) {
|
|
|
|
|
|
// 半透明遮罩:点它也能关(大屏上比只能点"关闭"更顺手)
|
|
|
|
|
|
Column()
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.height('100%')
|
2026-10-18 23:12:57 +08:00
|
|
|
|
.backgroundColor(FB_MASK)
|
2026-10-18 03:35:39 +08:00
|
|
|
|
.onClick((): void => {
|
|
|
|
|
|
this.sheetOpen = false;
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
FbMovieDetailSheet({
|
|
|
|
|
|
detail: this.sheetDetail,
|
|
|
|
|
|
loading: this.sheetLoading,
|
|
|
|
|
|
error: this.sheetError,
|
|
|
|
|
|
listRating: this.sheetRating,
|
|
|
|
|
|
listReviewCount: this.sheetReviewCount,
|
|
|
|
|
|
onClose: (): void => {
|
|
|
|
|
|
this.sheetOpen = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---- 影院列表浮层 ----
|
|
|
|
|
|
// ⚠️ 与详情浮层**同一个 Stack**、同一个层级:
|
|
|
|
|
|
// 两者不会同时打开(详情由点电影触发、影院由点"影院 N 家"触发),
|
|
|
|
|
|
// 所以不需要额外处理"谁盖谁"。
|
|
|
|
|
|
if (this.cinemaSheetOpen) {
|
|
|
|
|
|
Column()
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.height('100%')
|
2026-10-18 23:12:57 +08:00
|
|
|
|
.backgroundColor(FB_MASK)
|
2026-10-18 03:35:39 +08:00
|
|
|
|
.onClick((): void => {
|
|
|
|
|
|
this.cinemaSheetOpen = false;
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
FbCinemaSheet({
|
|
|
|
|
|
cinemas: this.cinemas,
|
|
|
|
|
|
onClose: (): void => {
|
|
|
|
|
|
this.cinemaSheetOpen = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---- 模块设置浮层(三条横线点开的那个)----
|
|
|
|
|
|
if (this.settingsOpen) {
|
|
|
|
|
|
Column()
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.height('100%')
|
2026-10-18 23:12:57 +08:00
|
|
|
|
.backgroundColor(FB_MASK)
|
2026-10-18 03:35:39 +08:00
|
|
|
|
.onClick((): void => {
|
|
|
|
|
|
this.settingsOpen = false;
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
FbModuleSettingsSheet({
|
|
|
|
|
|
title: '观影设置',
|
|
|
|
|
|
items: FB_SET_MOVIES,
|
|
|
|
|
|
onClose: (): void => {
|
|
|
|
|
|
this.settingsOpen = false;
|
|
|
|
|
|
},
|
|
|
|
|
|
onPick: (action: string): void => {
|
|
|
|
|
|
// 目前只有「影院管理」是已实现的;其余在菜单里显示「暂未开放」
|
|
|
|
|
|
if (action === 'cinema') {
|
|
|
|
|
|
this.settingsOpen = false;
|
|
|
|
|
|
this.cinemaSheetOpen = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.height('100%')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 页面主体(原 build 的内容,抽出来给 Stack 用) */
|
|
|
|
|
|
@Builder
|
|
|
|
|
|
pageContent() {
|
2026-10-12 10:06:55 +08:00
|
|
|
|
Scroll() {
|
|
|
|
|
|
Column() {
|
|
|
|
|
|
this.topBar()
|
|
|
|
|
|
|
|
|
|
|
|
// ---- 页头:标题 + 记一场电影 + 设置 ----
|
|
|
|
|
|
Row({ space: 8 }) {
|
|
|
|
|
|
Column({ space: 2 }) {
|
|
|
|
|
|
Text('观影记录')
|
|
|
|
|
|
.fontSize(FbMetric.fontTitle)
|
|
|
|
|
|
.fontWeight(FontWeight.Medium)
|
|
|
|
|
|
.fontColor(FbColors.ink)
|
|
|
|
|
|
Text('共 ' + this.movies.length.toString() + ' 部 · 按观影时间排序')
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(FbColors.faint)
|
|
|
|
|
|
}
|
|
|
|
|
|
.alignItems(HorizontalAlign.Start)
|
|
|
|
|
|
.layoutWeight(1)
|
|
|
|
|
|
|
|
|
|
|
|
FbPrimaryButton({
|
|
|
|
|
|
label: '记一场',
|
|
|
|
|
|
bg: this.accent,
|
|
|
|
|
|
icon: $r('app.media.fb_ic_plus')
|
|
|
|
|
|
})
|
2026-10-18 03:35:39 +08:00
|
|
|
|
// 🔴 设置按钮 = 网页版那种**三条横线**(☰);「影院」也不单独占抽屉位置,
|
|
|
|
|
|
// 它是观影模块的一部分,从设置菜单里进(与网页版一致)。
|
2026-10-12 10:06:55 +08:00
|
|
|
|
FbIconButton({
|
2026-10-18 03:35:39 +08:00
|
|
|
|
icon: $r('app.media.fb_ic_menu'),
|
2026-10-18 20:14:40 +08:00
|
|
|
|
tint: FbTint.muted,
|
2026-10-18 03:35:39 +08:00
|
|
|
|
dia: 36,
|
|
|
|
|
|
onTap: (): void => {
|
|
|
|
|
|
this.settingsOpen = true;
|
|
|
|
|
|
}
|
2026-10-12 10:06:55 +08:00
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12, bottom: 8 })
|
|
|
|
|
|
.alignItems(VerticalAlign.Center)
|
|
|
|
|
|
|
|
|
|
|
|
// ---- 搜索条 ----
|
|
|
|
|
|
Row({ space: 6 }) {
|
|
|
|
|
|
Image($r('app.media.fb_ic_search'))
|
|
|
|
|
|
.width(15)
|
|
|
|
|
|
.height(15)
|
|
|
|
|
|
.renderMode(ImageRenderMode.Template)
|
2026-10-18 20:14:40 +08:00
|
|
|
|
.colorFilter(fbTint(FbTint.faint))
|
2026-10-12 10:06:55 +08:00
|
|
|
|
Text('搜索片名、影院、观影人…')
|
|
|
|
|
|
.fontSize(FbMetric.fontSmall)
|
|
|
|
|
|
.fontColor(FbColors.faint)
|
|
|
|
|
|
.layoutWeight(1)
|
|
|
|
|
|
.maxLines(1)
|
|
|
|
|
|
}
|
|
|
|
|
|
.height(36)
|
|
|
|
|
|
.padding({ left: 12, right: 12 })
|
|
|
|
|
|
.backgroundColor(FbColors.inputBg)
|
|
|
|
|
|
.borderRadius(FbMetric.radiusS)
|
|
|
|
|
|
.margin({ left: FbMetric.pagePad, right: FbMetric.pagePad, bottom: 4 })
|
|
|
|
|
|
.alignItems(VerticalAlign.Center)
|
|
|
|
|
|
|
2026-10-18 03:35:39 +08:00
|
|
|
|
// ---- 影院数(可点:看影院列表)----
|
|
|
|
|
|
// 🔴 之前这是个**纯文字**(只能看数字);现在点它能看到名单 ——
|
|
|
|
|
|
// Web 端「影院管理」就在观影模块里,这里给它一个入口。
|
2026-10-12 10:06:55 +08:00
|
|
|
|
Row({ space: 6 }) {
|
|
|
|
|
|
Blank()
|
|
|
|
|
|
Text('影院 ' + this.cinemas.length.toString() + ' 家')
|
2026-10-11 22:07:48 +08:00
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(FbColors.faint)
|
2026-10-18 03:35:39 +08:00
|
|
|
|
Text('›')
|
|
|
|
|
|
.fontSize(FbMetric.fontSmall)
|
|
|
|
|
|
.fontColor(FbColors.faint)
|
2026-10-11 22:07:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
2026-10-18 03:35:39 +08:00
|
|
|
|
.onClick((): void => {
|
|
|
|
|
|
this.cinemaSheetOpen = true;
|
|
|
|
|
|
})
|
2026-10-12 10:06:55 +08:00
|
|
|
|
.padding({ left: FbMetric.pagePad + 4, right: FbMetric.pagePad + 4, top: 6, bottom: 6 })
|
|
|
|
|
|
|
2026-10-18 03:35:39 +08:00
|
|
|
|
|
|
|
|
|
|
// ---- 同步状态提示(只在有话说时显示,不占位)----
|
|
|
|
|
|
if (this.syncNote !== '') {
|
|
|
|
|
|
Row() {
|
|
|
|
|
|
Text(this.syncNote)
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(FbColors.faint)
|
|
|
|
|
|
.maxLines(2)
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, bottom: 6 })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-12 10:06:55 +08:00
|
|
|
|
// ---- 海报网格 ----
|
2026-10-18 03:35:39 +08:00
|
|
|
|
if (this.loading) {
|
|
|
|
|
|
// ⚠️ "加载中"与"暂无记录"必须分开:本页先读本地库(通常几毫秒),
|
|
|
|
|
|
// 所以 loading 几乎一闪而过;若把它和空态混在一起,
|
|
|
|
|
|
// 用户会看到"还没有观影记录"闪一下再变成列表。
|
|
|
|
|
|
Row() {
|
|
|
|
|
|
Text('正在读取本地数据…')
|
|
|
|
|
|
.fontSize(FbMetric.fontSmall)
|
|
|
|
|
|
.fontColor(FbColors.faint)
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding({ top: 40, bottom: 40 })
|
|
|
|
|
|
.justifyContent(FlexAlign.Center)
|
|
|
|
|
|
} else if (this.movies.length === 0) {
|
2026-10-12 10:06:55 +08:00
|
|
|
|
FbEmptyBox({
|
|
|
|
|
|
title: '还没有观影记录',
|
|
|
|
|
|
hint: '一家人一起看过的电影,都会以海报墙的方式排在这里',
|
|
|
|
|
|
icon: $r('app.media.fb_ic_movies')
|
|
|
|
|
|
})
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Grid() {
|
2026-10-18 03:35:39 +08:00
|
|
|
|
ForEach(this.movies, (mv: FbMovieListItem) => {
|
2026-10-12 10:06:55 +08:00
|
|
|
|
GridItem() {
|
|
|
|
|
|
this.movieCard(mv)
|
|
|
|
|
|
}
|
2026-10-18 03:35:39 +08:00
|
|
|
|
}, (mv: FbMovieListItem): string => 'mv_' + mv.id.toString())
|
2026-10-12 10:06:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
.columnsTemplate('1fr 1fr')
|
|
|
|
|
|
.columnsGap(12)
|
|
|
|
|
|
.rowsGap(12)
|
|
|
|
|
|
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, bottom: 24 })
|
|
|
|
|
|
// ⚠️ 不能给 layoutWeight:本页已整体放进 Scroll,
|
|
|
|
|
|
// Column 的高度由内容决定,layoutWeight(1) 会去分"剩余空间"(此时为 0)
|
|
|
|
|
|
// ⇒ Grid 高度塌成 0、整页只有顶栏和页头。
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.scrollBar(BarState.Off)
|
|
|
|
|
|
.edgeEffect(EdgeEffect.Spring)
|
|
|
|
|
|
}
|
2026-10-11 22:07:48 +08:00
|
|
|
|
}
|
2026-10-12 10:06:55 +08:00
|
|
|
|
.width('100%')
|
2026-10-12 12:06:00 +08:00
|
|
|
|
// 底部悬浮快捷栏的预留:它浮在内容之上,不留白会压住最后一屏
|
|
|
|
|
|
.padding({ bottom: FB_DOCK_RESERVE })
|
2026-10-11 22:07:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.height('100%')
|
2026-10-12 10:06:55 +08:00
|
|
|
|
.scrollBar(BarState.Off)
|
|
|
|
|
|
.edgeEffect(EdgeEffect.Spring)
|
2026-10-18 03:35:39 +08:00
|
|
|
|
// 🔴 滚动方向上报(驱动底部悬浮栏随滚收起 / 唤回)—— 见 common/FbScroll.ets
|
|
|
|
|
|
.onDidScroll(fbOnScroll)
|
|
|
|
|
|
|
|
|
|
|
|
// 🔴 触摸方向兜底(内容不足一屏的页面滚不动,只能靠手指位移判断)
|
|
|
|
|
|
// 与上面的 `onDidScroll` 互补、互不冲突 —— 见 common/FbScroll.ets
|
|
|
|
|
|
.onTouch(fbOnTouch)
|
2026-10-12 10:06:55 +08:00
|
|
|
|
.align(Alignment.TopStart)
|
2026-10-11 22:07:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 一张电影卡:海报位(2:3)+ 片名 + 元信息 + 同看的人 */
|
|
|
|
|
|
@Builder
|
2026-10-18 03:35:39 +08:00
|
|
|
|
movieCard(mv: FbMovieListItem) {
|
2026-10-11 22:07:48 +08:00
|
|
|
|
Column({ space: 0 }) {
|
2026-10-18 03:35:39 +08:00
|
|
|
|
// 海报位:有海报就放真图,没有/加载失败就画占位(与 Web 端一致)
|
2026-10-11 22:07:48 +08:00
|
|
|
|
Stack({ alignContent: Alignment.Center }) {
|
2026-10-18 03:35:39 +08:00
|
|
|
|
if (mv.posterKey !== '' && !this.posterFailed(mv.id)) {
|
|
|
|
|
|
// 🔴 真海报:走服务端 `media.php` 代理,令牌用 URL query 带过去
|
|
|
|
|
|
// (`Image()` 无法加自定义请求头,见 fbImageUrl 的说明)。
|
2026-10-18 16:01:41 +08:00
|
|
|
|
Image(fbImageSrc(mv.posterKey, FB_TOKEN_STORE.get()))
|
2026-10-11 22:07:48 +08:00
|
|
|
|
.width('100%')
|
|
|
|
|
|
.height('100%')
|
2026-10-18 03:35:39 +08:00
|
|
|
|
.objectFit(ImageFit.Cover)
|
|
|
|
|
|
// ⚠️ `alt` 给一张底图:图片没加载出来时先显示它,不会闪白块
|
|
|
|
|
|
.alt($r('app.media.fb_ic_movies'))
|
|
|
|
|
|
// ⚠️ 不 `syncLoad`:海报是大图,同步加载会卡住列表滚动。
|
|
|
|
|
|
// ⚠️ 加载失败**必须记下来**(见 onError):否则这张卡会一直
|
|
|
|
|
|
// 显示"半张白图",用户以为坏了,其实只是这张图取不到。
|
|
|
|
|
|
.onError((): void => {
|
|
|
|
|
|
this.markPosterFailed(mv.id);
|
2026-10-11 22:07:48 +08:00
|
|
|
|
})
|
|
|
|
|
|
} else {
|
2026-10-18 03:35:39 +08:00
|
|
|
|
// 无海报 / 加载失败:底图 + 图标/片名占位
|
2026-10-11 22:07:48 +08:00
|
|
|
|
Column()
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.height('100%')
|
|
|
|
|
|
.backgroundColor(FbColors.inputBg)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-18 03:35:39 +08:00
|
|
|
|
// 占位层:只在"没有海报"或"海报加载失败"时显示(有真图时不遮挡)
|
|
|
|
|
|
if (mv.posterKey === '' || this.posterFailed(mv.id)) {
|
|
|
|
|
|
Column({ space: 6 }) {
|
|
|
|
|
|
Image($r('app.media.fb_ic_movies'))
|
|
|
|
|
|
.width(22)
|
|
|
|
|
|
.height(22)
|
|
|
|
|
|
.renderMode(ImageRenderMode.Template)
|
2026-10-18 20:14:40 +08:00
|
|
|
|
.colorFilter(fbTint(FbTint.faint))
|
2026-10-11 22:07:48 +08:00
|
|
|
|
Text(mv.title)
|
|
|
|
|
|
.fontSize(FbMetric.fontSmall)
|
|
|
|
|
|
.fontColor(FbColors.muted)
|
|
|
|
|
|
.textAlign(TextAlign.Center)
|
|
|
|
|
|
.maxLines(3)
|
|
|
|
|
|
.padding({ left: 10, right: 10 })
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.aspectRatio(0.75)
|
|
|
|
|
|
.clip(true)
|
|
|
|
|
|
.borderRadius({ topLeft: FbMetric.radius, topRight: FbMetric.radius })
|
|
|
|
|
|
|
|
|
|
|
|
// 卡片正文
|
|
|
|
|
|
Column({ space: 5 }) {
|
|
|
|
|
|
Text(mv.title)
|
|
|
|
|
|
.fontSize(FbMetric.fontSmall)
|
|
|
|
|
|
.fontWeight(FontWeight.Medium)
|
|
|
|
|
|
.fontColor(FbColors.ink)
|
|
|
|
|
|
.maxLines(2)
|
|
|
|
|
|
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
|
|
|
|
|
|
|
|
|
|
|
Row({ space: 6 }) {
|
2026-10-18 03:35:39 +08:00
|
|
|
|
// ⚠️ 显示上只取「日期 + 时:分」:接口给的是完整 DATETIME(含秒),
|
|
|
|
|
|
// 秒在卡片上纯属噪音。
|
|
|
|
|
|
Text(shortTime(mv.startsAt))
|
2026-10-11 22:07:48 +08:00
|
|
|
|
.fontSize(10)
|
|
|
|
|
|
.fontColor(FbColors.faint)
|
|
|
|
|
|
.maxLines(1)
|
|
|
|
|
|
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
|
|
|
|
|
|
if (mv.cinemaName.length > 0) {
|
|
|
|
|
|
Row({ space: 4 }) {
|
|
|
|
|
|
FbDot({ dia: 6, fillC: mv.cinemaColor })
|
|
|
|
|
|
Text(mv.cinemaName)
|
|
|
|
|
|
.fontSize(10)
|
|
|
|
|
|
.fontColor(mv.cinemaColor)
|
|
|
|
|
|
.maxLines(1)
|
|
|
|
|
|
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
|
|
|
|
|
.layoutWeight(1)
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Row({ space: 8 }) {
|
|
|
|
|
|
if (mv.reviewCount > 0) {
|
2026-10-18 03:35:39 +08:00
|
|
|
|
FbRating({ score: mv.reviewAvg, max: 10 })
|
2026-10-11 22:07:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
Blank()
|
2026-10-18 03:35:39 +08:00
|
|
|
|
// 同看人数(只出数字,空间紧;点头像看详情是后续的事)
|
|
|
|
|
|
if (mv.attendeeCount > 0) {
|
|
|
|
|
|
Text(mv.attendeeCount.toString() + ' 人同看')
|
|
|
|
|
|
.fontSize(10)
|
|
|
|
|
|
.fontColor(FbColors.faint)
|
|
|
|
|
|
.maxLines(1)
|
2026-10-11 22:07:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.alignItems(VerticalAlign.Center)
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding(10)
|
|
|
|
|
|
.alignItems(HorizontalAlign.Start)
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.backgroundColor(FbColors.surface)
|
|
|
|
|
|
.borderRadius(FbMetric.radius)
|
|
|
|
|
|
.clip(true)
|
2026-10-18 03:35:39 +08:00
|
|
|
|
// 🔴 点卡片 → 打开详情浮层(用户 2026-10-12 反馈"点不进去")
|
|
|
|
|
|
.onClick((): void => {
|
|
|
|
|
|
this.openDetail(mv);
|
|
|
|
|
|
})
|
2026-10-11 22:07:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|