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 为空(设备未连/未解锁)。
   这四项全是观感与手感,静态断言证明不了「点得着、滑得动、看着舒服」,
   待设备连上后补真机探针(重点:真的点「今」、真的滑列表、量两列实际间距)。
This commit is contained in:
2026-10-26 07:28:06 +08:00
parent 7ae34e6e23
commit b84ea06b73
11 changed files with 732 additions and 169 deletions
@@ -1,28 +1,35 @@
/**
* 家书 FamilyBook · 6×4 服务卡片(观影 · 上概述/新建,下滚动列表)
* 家书 FamilyBook · 6×4 服务卡片(观影 · 上概述,下**两列海报墙**)—— v92 / v95
*
* 用户 2026-10-25:「观影卡片,加4x4,和4x6,上面概述,加新建按钮,
* 下面瀑布流,就跟观影模块类似,可以滑下去看全部电影。」
* ⇒ 上半:标题 + 更新时间 + 「+ 新建」(路由进 app 直开「记一场」);
* 下半:`List`(卡片**不支持 Scroll、只支持 List**,见 FbCard4x4 的注释)
* 装全部观影(数据侧倒序裁到 30 条),整卡可点进模块。
* 用户 2026-10-25:「观影应该显示图片……实际上应该用大图的形式显示,
* 就做成和观影模式一样,不行吗?」
* ⇒ 与 4×4 同一套海报墙。
*
* ⚠️ 卡片不能自己发网络请求:只画 `fbMovieCardBuild()` 推过来的字段。
* ⚠️ ForEach key 带 version 前缀(v89 教训,不重绘就是它的锅)。
* 🔴 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 storageMvB2 = new LocalStorage();
let storageMvP6 = new LocalStorage();
/** 列表一行 */
class FbMovieRow9b {
/** 海报墙一格 */
class FbMovieCell9b {
key: string = '';
title: string = '';
dateText: string = '';
cinema: string = '';
rating: string = '';
/** `memory://` 的内存键;空串 = 没图,画占位 */
img: string = '';
}
@Entry(storageMvB2)
@Entry(storageMvP6)
@Component
struct FbMovieCard6x4 {
@LocalStorageProp('state') state: string = 'loading';
@@ -33,9 +40,9 @@ struct FbMovieCard6x4 {
@LocalStorageProp('listJson') listJson: string = '[]';
@LocalStorageProp('version') version: string = '';
private rows(): FbMovieRow9b[] {
private rows(): FbMovieCell9b[] {
try {
const all: FbMovieRow9b[] = JSON.parse(this.listJson) as FbMovieRow9b[];
const all: FbMovieCell9b[] = JSON.parse(this.listJson) as FbMovieCell9b[];
return all;
} catch (err) {
return [];
@@ -59,43 +66,67 @@ struct FbMovieCard6x4 {
});
}
/** 列表单行:日期 + 片名/影院(+评分) */
/** 图下那行小字:`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
listRow(item: FbMovieRow9b) {
Row({ space: 8 }) {
Text(item.dateText !== '' ? item.dateText : '—')
.fontSize(9)
.fontColor('#A8988A')
.width(46)
.maxLines(1)
Column({ space: 2 }) {
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(13)
.fontColor('#2C231D')
.fontSize(10)
.fontWeight(FontWeight.Medium)
.fontColor('#FFFFFF')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
if (item.cinema !== '') {
Text(item.cinema)
.fontSize(9)
.fontColor('#A8988A')
.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%')
}
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
if (item.rating !== '') {
Text(item.rating)
.fontSize(10)
.fontColor('#C08A3E')
.maxLines(1)
}
.width('100%')
.padding({ left: 6, right: 6, top: 3, bottom: 3 })
.backgroundColor('#66000000')
}
.alignItems(VerticalAlign.Center)
.width('100%')
.padding({ left: 8, right: 8, top: 5, bottom: 5 })
.borderRadius(8)
.backgroundColor('#F6F2F7')
.aspectRatio(0.68)
.borderRadius(10)
.clip(true)
}
/** 空态 */
@@ -127,7 +158,6 @@ struct FbMovieCard6x4 {
.fontSize(9)
.fontColor('#A8988A')
.maxLines(1)
/* ⭐ 新建按钮(子组件 onClick 优先于整卡,不会连带进 app) */
Button('+')
.fontSize(13)
.fontColor('#FFFFFF')
@@ -155,21 +185,33 @@ struct FbMovieCard6x4 {
}
.width('100%')
Divider()
.strokeWidth(0.5)
.color('#EFE6D8')
// ===== 全部观影(可滚动)=====
// ===== 海报墙(三列,可滚动)=====
if (this.rows().length === 0) {
this.emptyHint()
} else {
List({ space: 5 }) {
ForEach(this.rows(), (item: FbMovieRow9b) => {
List({ space: 8 }) {
ForEach(this.rows(), (item: FbMovieCell9b) => {
ListItem() {
this.listRow(item)
this.posterCell(item)
}
}, (item: FbMovieRow9b): string => `ml_${this.version}_${item.key}`)
}, (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)
@@ -180,7 +222,6 @@ struct FbMovieCard6x4 {
.padding(12)
.backgroundColor('#FFFFFF')
.borderRadius(16)
// ⭐ 整卡可点
.onClick((): void => this.openApp())
}
}