Files
FamilyBook_harmonyos/entry/src/main/ets/pages/card/FbMovieCard6x4.ets
T
laoyang b84ea06b73 v95 服务卡片四项:日程卡「今」按钮放大、观影/任务 2x4 列表可滑动、观影 4x4/6x4 补列间距且 6x4 改两列
#1

用户提四项(2026-10-26):
  「日程的右上角那个今天的今,特别不好点,一不小心就到日程里面去了……
    是不是可以把按钮弄的大一点。」
  「观影的卡片,2x4,右侧观影的列表,不能上下滑动,第三个被截断,很难看,
    如果显示多个,就要能滑动,如果不能滑动,就显示两个。」
  「任务的 2x4 卡片,也是一样的。」
  「观影的 4x4 卡片,和 4x6 卡片,两列图片挤在一起,中间没有空隙,应该缩小一点,
    这个大小,我感觉应该根据屏幕的大小来。4x6,宽度都是一样的,为啥要弄成三列?」

① 日程卡「今」按钮 22x22 → 32x32
   病根:22 的圆比最小建议点击区小一半,手指偏一点就落到卡片本体的
   `.onClick(openApp)` 上 ⇒ 「想回今天」变成「进了日程详情」。
   面积 484 → 1024(2.1 倍);同一份 sideBtn() 里的「+」一并放大,
   翻页键 ‹ › 本来 32,现在四个按钮手感一致。
   为什么是 32 不是 40:4x4 卡高度紧,顶栏与内容区竖向堆叠
   (内容区 layoutWeight(1)),顶栏长高多少内容区就少多少。
   必须改 width/height 本身 —— 卡片里这是 Button,可点区就是自己的宽高。

②③ 观影/任务 2x4 右半 Column → List(可滚动)
   病根不在条数而在容器:Column 根本不滚动 + 硬截 3 条,
   3x(38+10=48) + 2x6 = 156vp 而可用仅 ≈130vp ⇒ 第三条被压扁/裁掉。
   条数上限 3 → MR_2X4_MAX=12 / TK_2X4_MAX=30;滚动条 BarState.Auto
   (条目少时不露空滚动条)。数据侧 FbCardMediaData 两处 `rows.length < 4`
   同步放到 30,否则卡上拿到的还是 4 条,改了容器也滑不出东西。

④ 观影 4x4 / 6x4 补列间距 + 6x4 三列改两列
   病根:`List({ space: 8 })` 的 space 在 lanes 多列模式下只管行间距,
   列与列之间不会自动留缝;海报格 .width('100%') + .aspectRatio(0.68)
   又贴着 lane 边排 ⇒ 两列严丝合缝贴死。
   改法:.lanes(2) → .lanes(2, 8),第二个位置参数才是列间距 gutter。
   ⚠️ gutter 是位置参数(SDK list.d.ts: lanes(value, gutter?: Dimension),
      Dimension = PX|VP|FP|LPX|Percentage|Resource)⇒ 传数字,
      写成 { gutter: 8 } 是类型错误(中途踩到,编译期才炸)。
   6x4 同时从 .lanes(3) 改成两列,与 4x4 一致;文件头那句
   「这块卡更宽、一排放三张」已过时,已注明别再照着它改回去。

同批带上 v92 未提交的观影海报墙实现(此前只在工作树里,从未提交过):
  - 新增 data/FbCardImages.ets(下载 + desiredSize 缩图 + fd 管理)
  - FbCardMediaData / FbFormAbility / FbCardForms 里的 formImages 管道
  - string.json 卡片名带尺寸(「家书 · 日程 4×4」/「家书 · 观影 4×4」)
  ⚠️ FbCardImages.ets 已被 FbCardMediaData import,不一起提交就编译不过。

验证:
  - 静态守卫 _fb_arkts_guard.js:1303 通过 / 0 失败
    (新增 groupV95() 共 28 条,钉住按钮尺寸、List 容器、条数上限、
      两列 + gutter>0、且不许写成 { gutter: N } 对象)
  - 反向验证 _fb_v59d_falsify.py:289 通过 / 0 失败
    (新增 9 条注入,逐条证明上述断言真的会红)
  - 真编译 hvigorw assembleHap:BUILD SUCCESSFUL,HAP 产物已生成

⚠️ 真机验收未做:hdc list targets 为空(设备未连/未解锁)。
   这四项全是观感与手感,静态断言证明不了「点得着、滑得动、看着舒服」,
   待设备连上后补真机探针(重点:真的点「今」、真的滑列表、量两列实际间距)。
2026-10-26 07:28:06 +08:00

228 lines
7.3 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 家书 FamilyBook · 6×4 服务卡片(观影 · 上概述,下**两列海报墙**)—— v92 / v95
*
* 用户 2026-10-25:「观影应该显示图片……实际上应该用大图的形式显示,
* 就做成和观影模式一样,不行吗?」
* ⇒ 与 4×4 同一套海报墙。
*
* 🔴 v95(用户 2026-10-26):「4x6,宽度都是一样的,为啥要弄成三列?」
* ⇒ 列数从 3 改成 **2**(与 4×4 一致),并补上列间距 `gutter`。
* ⚠️ 文件头原来写的是"这块卡更宽、一排放三张"——那句已经**过时**,
* 别再照着它改回 `.lanes(3)`。
*
* 🔴 卡片里的图不能写 http 地址,只能 `Image('memory://' + img)`
* (图由 `FbCardMediaData` 下载+缩图、以 `formImages` 的 fd 交过来)。
* ⚠️ 卡片不支持 `Scroll`,只支持 `List` ⇒ 多列用 `List().lanes(n, gutter)`,别换成 Grid。
* ⚠️ ForEach key 带 version 前缀(v89 教训)。
*/
let storageMvP6 = new LocalStorage();
/** 海报墙一格 */
class FbMovieCell9b {
key: string = '';
title: string = '';
dateText: string = '';
cinema: string = '';
rating: string = '';
/** `memory://` 的内存键;空串 = 没图,画占位 */
img: string = '';
}
@Entry(storageMvP6)
@Component
struct FbMovieCard6x4 {
@LocalStorageProp('state') state: string = 'loading';
@LocalStorageProp('hint') hint: string = '';
@LocalStorageProp('updatedAt') updatedAt: string = '';
@LocalStorageProp('yearCount') yearCount: number = 0;
@LocalStorageProp('totalCount') totalCount: number = 0;
@LocalStorageProp('listJson') listJson: string = '[]';
@LocalStorageProp('version') version: string = '';
private rows(): FbMovieCell9b[] {
try {
const all: FbMovieCell9b[] = JSON.parse(this.listJson) as FbMovieCell9b[];
return all;
} catch (err) {
return [];
}
}
private openApp(): void {
postCardAction(this, {
action: 'router',
abilityName: 'EntryAbility',
params: { fbModule: 'movies' }
});
}
/** 「+ 新建」→ 进 app 直开「记一场」浮层(EntryAbility → modCreate 通道) */
private addOne(): void {
postCardAction(this, {
action: 'router',
abilityName: 'EntryAbility',
params: { fbModule: 'movies', widgetAction: 'add' }
});
}
/** 图下那行小字:`10月24日 · 万象城`(都空就不画) */
private subText(item: FbMovieCell9b): string {
let s: string = item.dateText;
if (item.cinema !== '') {
s = s !== '' ? s + ' · ' + item.cinema : item.cinema;
}
return s;
}
/** 一格格:大图 + 底部黑条上的片名/日期 */
@Builder
posterCell(item: FbMovieCell9b) {
Stack({ alignContent: Alignment.Bottom }) {
if (item.img !== '') {
// ⚠️ 只能 memory://(见文件头):写 http 地址在卡片里是一张空白
Image('memory://' + item.img)
.width('100%')
.height('100%')
.objectFit(ImageFit.Cover)
.borderRadius(10)
} else {
Column() {
Text(item.title)
.fontSize(10)
.fontColor('#7A4E8C')
.maxLines(3)
.textAlign(TextAlign.Center)
.padding({ left: 4, right: 4 })
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor('#EDE6F1')
.borderRadius(10)
}
Column({ space: 1 }) {
Text(item.title)
.fontSize(10)
.fontWeight(FontWeight.Medium)
.fontColor('#FFFFFF')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.width('100%')
if (this.subText(item) !== '') {
Text(this.subText(item))
.fontSize(8)
.fontColor('#FFFFFF')
.opacity(0.8)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.width('100%')
}
}
.width('100%')
.padding({ left: 6, right: 6, top: 3, bottom: 3 })
.backgroundColor('#66000000')
}
.width('100%')
.aspectRatio(0.68)
.borderRadius(10)
.clip(true)
}
/** 空态 */
@Builder
emptyHint() {
Column() {
Text(this.hint !== '' ? this.hint : '还没有观影记录')
.fontSize(12)
.fontColor(this.hint !== '' ? '#C0533F' : '#A8988A')
.maxLines(3)
.textAlign(TextAlign.Center)
}
.width('100%')
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
}
build() {
Column({ space: 6 }) {
// ===== 顶栏:标题 + 更新时间 + + 新建 =====
Row({ space: 6 }) {
Text('观影')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#2C231D')
.maxLines(1)
.layoutWeight(1)
Text(this.updatedAt)
.fontSize(9)
.fontColor('#A8988A')
.maxLines(1)
Button('+')
.fontSize(13)
.fontColor('#FFFFFF')
.width(26)
.height(26)
.borderRadius(13)
.padding(0)
.backgroundColor('#7A4E8C')
.onClick((): void => this.addOne())
}
.width('100%')
// ===== 概述行 =====
Row({ space: 8 }) {
Text(`今年 ${this.yearCount} 场`)
.fontSize(11)
.fontColor('#7A4E8C')
.fontWeight(FontWeight.Medium)
Text(`共 ${this.totalCount} 条`)
.fontSize(10)
.fontColor(this.hint !== '' ? '#C0533F' : '#A8988A')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.layoutWeight(1)
}
.width('100%')
// ===== 海报墙(三列,可滚动)=====
if (this.rows().length === 0) {
this.emptyHint()
} else {
List({ space: 8 }) {
ForEach(this.rows(), (item: FbMovieCell9b) => {
ListItem() {
this.posterCell(item)
}
}, (item: FbMovieCell9b): string => `mp_${this.version}_${item.key}`)
}
/* 🔴🔴 v95(用户 2026-10-26):「4x6,宽度都是一样的,为啥要弄成三列?」
—— 原来是 `.lanes(3)`(文件头那句"这块卡更宽、一排放三张")。
⇒ 改成**两列**,与 4×4 一致:
· 用户明确要求,且三列时每张海报明显更小、更挤,观感差;
· 两列 + 更高的卡高 ⇒ 一屏能看到的行数更多,反而比三列看得多。
⚠️ 同时补 `gutter`:`List({ space: 8 })` 的 `space` 在 lanes 多列
模式下只管**行间距**,列缝必须靠 `lanes(count, gutter)` 给,
否则两列又会贴死(与 4×4 同一个坑,见 FbMovieCard4x4 里的详细说明)。
`gutter` 是**位置参数**(`Dimension`),直接传数字,**不是** `{ gutter: 8 }`。
⚠️ 别再改回 `.lanes(3)`:列数一多,同一块卡里的海报就被压到
看不清片名,而用户要的恰恰是"大图"。 */
.lanes(2, 8)
.alignListItem(ListItemAlign.Start)
.width('100%')
.layoutWeight(1)
.scrollBar(BarState.Off)
}
}
.width('100%')
.height('100%')
.padding(12)
.backgroundColor('#FFFFFF')
.borderRadius(16)
.onClick((): void => this.openApp())
}
}