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
+30 -13
View File
@@ -166,32 +166,49 @@ struct FbCard4x4 {
private pageBtn(label: string, act: string) {
Button() {
Text(label)
.fontSize(14)
.fontSize(15)
.fontWeight(FontWeight.Medium)
.fontColor('#5A4A3D')
}
.width(22)
.height(22)
.borderRadius(11)
.width(32)
.height(32)
.borderRadius(16)
.padding(0)
.backgroundColor('#F3ECE2')
/* ⚠️ 无需阻止冒泡:onClick 非冒泡、子组件优先 → 点翻页不会连带打开应用 */
.onClick((): void => this.page(act))
}
/** 今天时是「+」(新建日程);翻到别的日期时是「今」(回到今天) */
/** 今天时是「+」(新建日程);翻到别的日期时是「今」(回到今天)
*
* 🔴🔴 v95(用户 2026-10-26):「日程的右上角那个今天的今,特别不好点,
* 一不小心就到日程里面去了,其实我只不过想看看今天的日程有哪些,
* 是不是可以把按钮弄的大一点。」
*
* ⚠️ 原来这里是 `22x22` —— 直径 22 的圆,比系统建议的最小点击区(40vp 左右)
* 小了一半,手指稍微偏一点就落到**卡片本体**的 `.onClick(openApp)` 上,
* 于是"想回今天"变成"进了日程详情",正是用户描述的现象。
*
* ⚠️ 为什么是 32 而不是 40:4×4 卡高度很紧,而顶栏和内容区是**竖向堆叠**
* (内容区 `layoutWeight(1)`)。顶栏长高多少,内容区就少多少 ——
* 做成 40 会把时间轴挤掉一截。32 是"热区显著变大、又不吃掉内容"的折中:
* 面积从 22×22=484 涨到 32×32=1024(**2.1 倍**),
* 且与翻页键同尺寸,三个按钮的点击手感一致。
* ⚠️ 别只放大 `hitTestBehavior` 之类而不动尺寸:卡片是 `Button`,
* 它的可点区域就是自己的宽高,不改宽高等于没改。
*/
@Builder
private sideBtn() {
if (this.isToday) {
Button() {
Text('+')
.fontSize(14)
.fontSize(15)
.fontWeight(FontWeight.Medium)
.fontColor('#FFFFFF')
}
.width(22)
.height(22)
.borderRadius(11)
.width(32)
.height(32)
.borderRadius(16)
.padding(0)
.backgroundColor('#A84A28')
.onClick((): void => {
@@ -204,13 +221,13 @@ struct FbCard4x4 {
} else {
Button() {
Text('今')
.fontSize(11)
.fontSize(13)
.fontWeight(FontWeight.Medium)
.fontColor('#FFFFFF')
}
.width(22)
.height(22)
.borderRadius(11)
.width(32)
.height(32)
.borderRadius(16)
.padding(0)
.backgroundColor('#A84A28')
.onClick((): void => this.page('today'))
+21 -13
View File
@@ -161,32 +161,40 @@ struct FbCard6x4 {
private pageBtn(label: string, act: string) {
Button() {
Text(label)
.fontSize(14)
.fontSize(15)
.fontWeight(FontWeight.Medium)
.fontColor('#5A4A3D')
}
.width(22)
.height(22)
.borderRadius(11)
.width(32)
.height(32)
.borderRadius(16)
.padding(0)
.backgroundColor('#F3ECE2')
/* ⚠️ 无需阻止冒泡:onClick 非冒泡、子组件优先 → 点翻页不会连带打开应用 */
.onClick((): void => this.page(act))
}
/** 今天时是「+」(新建日程);翻到别的日期时是「今」(回到今天) */
/** 今天时是「+」(新建日程);翻到别的日期时是「今」(回到今天)
*
* 🔴🔴 v95(用户 2026-10-26):「日程的右上角那个今天的今,特别不好点,
* 一不小心就到日程里面去了……是不是可以把按钮弄的大一点。」
* 原来 22×22,比最小建议点击区小一半,手指偏一点就落到卡片本体的
* `.onClick(openApp)` 上 ⇒ "想回今天"变成"进了日程详情"。
* ⇒ 放大到 32×32(面积 484→1024,2.1 倍),与 4×4 卡同一尺寸、同一手感。
* ⚠️ 必须改 width/height 本身:卡片里这是 `Button`,可点区就是自己的宽高,
* 光改 padding / 字号不会扩大热区。 */
@Builder
private sideBtn() {
if (this.isToday) {
Button() {
Text('+')
.fontSize(14)
.fontSize(15)
.fontWeight(FontWeight.Medium)
.fontColor('#FFFFFF')
}
.width(22)
.height(22)
.borderRadius(11)
.width(32)
.height(32)
.borderRadius(16)
.padding(0)
.backgroundColor('#A84A28')
.onClick((): void => {
@@ -199,13 +207,13 @@ struct FbCard6x4 {
} else {
Button() {
Text('今')
.fontSize(11)
.fontSize(13)
.fontWeight(FontWeight.Medium)
.fontColor('#FFFFFF')
}
.width(22)
.height(22)
.borderRadius(11)
.width(32)
.height(32)
.borderRadius(16)
.padding(0)
.backgroundColor('#A84A28')
.onClick((): void => this.page('today'))
@@ -18,6 +18,27 @@ class FbMovieRow4 {
rating: string = '';
}
/**
* 🔴🔴 v95(用户 2026-10-26):「观影的卡片,2x4,右侧观影的列表,不能上下滑动,
* 第三个被截断,很难看,如果显示多个,就要能滑动,如果不能滑动,就显示两个。」
*
* ⚠️ 病根不在"条数写错了",而在**容器用错了**:
* 原来右半是 `Column({ space: 6 })` + `justifyContent(FlexAlign.Center)`,
* `Column` **根本不滚动**,而 `recent()` 又硬截到 3 条 ——
* 3 × (38 色条 + 上下 padding 5+5 = 48) + 2 × 6 间隔 = **156vp**,
* 而 2×4 卡右半可用高度只有 ≈130vp ⇒ 第三条被父容器直接压扁/裁掉,
* 就是用户看到的"第三个被截断,很难看"。
*
* ⇒ 改成 `List`(可滚动)+ 把条数上限提到 `MR_2X4_MAX`。
* 这样:条目多于可见高度时**能上下滑**(用户要的"显示多个就能滑动"),
* 同时可见高度天然只容得下完整的行(List 不会把行压扁,行高由内容定)。
*
* ⚠️ 不要退回 `Column` + `slice(0,2)` 的写法:那是"不能滑就显示两个",
* 用户明确更倾向"能滑"(他先说的是"如果显示多个,就要能滑动")。
* 真要兜底也只能是 list 空出来,不能靠截条数硬凑。
*/
const MR_2X4_MAX = 12;
@Entry(storageMv4)
@Component
struct FbMovieCard2x4 {
@@ -30,11 +51,11 @@ struct FbMovieCard2x4 {
/* 🔴 v89 教训:ForEach key 必须带 version 前缀,否则下次推送不重绘 */
@LocalStorageProp('version') version: string = '';
/** 右半最多三条(数据侧已裁到 4,这里再兜底截断) */
/** 右半最多 MR_2X4_MAX 条(数据侧会裁;这里只兜底防御异常大数据) */
private recent(): FbMovieRow4[] {
try {
const all: FbMovieRow4[] = JSON.parse(this.recentJson) as FbMovieRow4[];
return all.slice(0, 3);
return all.slice(0, MR_2X4_MAX);
} catch (err) {
return [];
}
@@ -166,19 +187,28 @@ struct FbMovieCard2x4 {
.strokeWidth(0.5)
.color('#EFE6D8')
// ===== 右半:最近观影(不滚动)=====
// ===== 右半:最近观影(🔴 v95 改成可滚动,见 MR_2X4_MAX 的注释)=====
Column() {
if (this.recent().length === 0) {
this.emptyHint()
} else {
Column({ space: 6 }) {
List({ space: 6 }) {
ForEach(this.recent(), (item: FbMovieRow4) => {
this.recentRow(item)
ListItem() {
this.recentRow(item)
}
.width('100%')
}, (item: FbMovieRow4) => `mv_${this.version}_${item.key}`)
}
.width('100%')
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
/* ⚠️ 必须 `layoutWeight(1)` + 固定 `BarState.Auto`:
前者让它撑满右半高度、才有"可滚"的视口;
后者让滚动条只在真能滚时出现(条目少的时候不露一条空滚动条)。
⚠️ 不能加 `justifyContent(Center)`:List 上那是内容对齐,
内容比视口高时会变成"上边也被推到看不见"的地方,首行反而被裁。 */
.scrollBar(BarState.Auto)
.cachedCount(4)
}
}
.layoutWeight(1)
+100 -50
View File
@@ -1,28 +1,35 @@
/**
* 家书 FamilyBook · 4×4 服务卡片(观影 · 上概述/新建,下滚动列表)
* 家书 FamilyBook · 4×4 服务卡片(观影 · 上概述,下**海报墙**)—— v92
*
* 用户 2026-10-25:「观影卡片,加4x4,和4x6,上面概述,加新建按钮,
* 下面瀑布流,就跟观影模块类似,可以滑下去看全部电影。」
* ⇒ 上半:标题 + 更新时间 + 「+ 新建」(路由进 app 直开「记一场」);
* 下半:`List`(卡片**不支持 Scroll、只支持 List**,见 FbCard4x4 的注释)
* 装全部观影(数据侧倒序裁到 30 条),整卡可点进模块。
* 用户 2026-10-25:「观影应该显示图片,左侧应该显示图片。实际上应该用大图的形式显示,
* 就做成和观影模式一样,不行吗?」
* ⇒ 下半从"文字行列表"改成**和观影模块一致的海报墙**:两列大图(2:3 海报),
* 片名压在图上(黑色半透明条),整卡可点进模块。
*
* ⚠️ 卡片不能自己发网络请求:只画 `fbMovieCardBuild()` 推过来的字段。
* ⚠️ ForEach key 带 version 前缀(v89 教训,不重绘就是它的锅)。
* 🔴🔴 卡片里的图**不能写 http 地址**(ArkTS 卡片不支持网络路径前缀和 file://):
* 图由 `FbCardMediaData` 预先下载/缩图、以 `formImages` 的 fd 交给这里,
* 本页只写 `Image('memory://' + item.img)`。看不到节点名就看不到图的坑,
* 全在 `data/FbCardImages.ets` 的文件头里。
*
* ⚠️ 卡片不支持 `Scroll`,只支持 `List` —— 海报墙用 `List().lanes(2)` 做两列,
* 既保住了滚动,也保住了"大图"(`Grid` 在卡片里的支持不可靠,别换)。
* ⚠️ ForEach key 带 version 前缀(v89 教训:key 撞车 = 静默不重绘)。
*/
let storageMvB = new LocalStorage();
let storageMvP4 = new LocalStorage();
/** 列表一行 */
class FbMovieRow9a {
/** 海报墙一格 */
class FbMovieCell9a {
key: string = '';
title: string = '';
dateText: string = '';
cinema: string = '';
rating: string = '';
/** `memory://` 的内存键;空串 = 没图,画占位 */
img: string = '';
}
@Entry(storageMvB)
@Entry(storageMvP4)
@Component
struct FbMovieCard4x4 {
@LocalStorageProp('state') state: string = 'loading';
@@ -33,9 +40,9 @@ struct FbMovieCard4x4 {
@LocalStorageProp('listJson') listJson: string = '[]';
@LocalStorageProp('version') version: string = '';
private rows(): FbMovieRow9a[] {
private rows(): FbMovieCell9a[] {
try {
const all: FbMovieRow9a[] = JSON.parse(this.listJson) as FbMovieRow9a[];
const all: FbMovieCell9a[] = JSON.parse(this.listJson) as FbMovieCell9a[];
return all;
} catch (err) {
return [];
@@ -59,43 +66,70 @@ struct FbMovieCard4x4 {
});
}
/** 列表单行:日期 + 片名/影院(+评分) */
/** 图下那行小字:`10月24日 · 万象城`(都空就不画) */
private subText(item: FbMovieCell9a): string {
let s: string = item.dateText;
if (item.cinema !== '') {
s = s !== '' ? s + ' · ' + item.cinema : item.cinema;
}
return s;
}
/** 一格格:大图 + 底部黑条上的片名/日期 */
@Builder
listRow(item: FbMovieRow9a) {
Row({ space: 8 }) {
Text(item.dateText !== '' ? item.dateText : '—')
.fontSize(9)
.fontColor('#A8988A')
.width(46)
.maxLines(1)
Column({ space: 2 }) {
posterCell(item: FbMovieCell9a) {
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)
}
// 底部信息条(黑 40%),压在图上,省掉图上图下两份高度
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')
// 2:3 竖版海报(和观影模块同一个比例)
.aspectRatio(0.68)
.borderRadius(10)
.clip(true)
}
/** 空态 */
@@ -155,21 +189,37 @@ struct FbMovieCard4x4 {
}
.width('100%')
Divider()
.strokeWidth(0.5)
.color('#EFE6D8')
// ===== 全部观影(可滚动)=====
// ===== 海报墙(两列,可滚动)=====
if (this.rows().length === 0) {
this.emptyHint()
} else {
List({ space: 5 }) {
ForEach(this.rows(), (item: FbMovieRow9a) => {
List({ space: 8 }) {
ForEach(this.rows(), (item: FbMovieCell9a) => {
ListItem() {
this.listRow(item)
this.posterCell(item)
}
}, (item: FbMovieRow9a): string => `ml_${this.version}_${item.key}`)
}, (item: FbMovieCell9a): string => `mp_${this.version}_${item.key}`)
}
/* 🔴🔴 v95(用户 2026-10-26):「观影的 4x4 卡片……两列图片挤在一起,
中间没有空隙,应该缩小一点。」
⚠️ 病根:`List({ space: 8 })` 的 `space` 在 **lanes 多列模式**下只管
**行间距**,列与列之间**不会**自动留缝。而海报格是
`.width('100%')` + `.aspectRatio(0.68)` —— 宽度由 lane 决定、
lane 又贴着排 ⇒ 两列严丝合缝贴死,正是用户看到的样子。
⇒ 必须用 `lanes(count, gutter)` 显式给**列间距**。
`gutter` 才是 lanes 模式下的列缝;
这里与行间距同取 8,横竖一致,看着才是"网格"而不是"拼图"。
⚠️⚠️ `gutter` 是**位置参数**(SDK: `lanes(value, gutter?: Dimension)`,
`Dimension = PX|VP|FP|LPX|Percentage|Resource`),直接传数字 `8`。
**不能**写成 `{ gutter: 8 }` —— 那是别的框架的写法,ArkTS 里是类型错误。
⚠️ 别指望调 `space` 或给格子加 margin 来凑:
加 margin 会让 `width('100%')` 溢出 lane,右列被截掉一条。
⚠️ 也别改 `.aspectRatio` 来"缩小图片":比例一变海报就被拉扁,
用户要的是"中间有空隙",不是"图变小变形"。 */
.lanes(2, 8)
.alignListItem(ListItemAlign.Start)
.width('100%')
.layoutWeight(1)
.scrollBar(BarState.Off)
@@ -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())
}
}
@@ -21,6 +21,14 @@ class FbTaskRow4 {
overdue: boolean = false;
}
/** 右半可滚动列表的条数上限。
*
* 🔴 v95:配套 `rows` 改成 `List` 一起用。上限必须**和观影 2×4 同一个量级**,
* 否则"能滑"只是名义上能滑(数据侧先砍到 4 条,卡片就只滑得到 4 条)。
* 30 与 4×4/6×4 的网格列表一致,数据侧也只给这么多。
*/
const TK_2X4_MAX = 30;
@Entry(storageTk4)
@Component
struct FbTaskCard2x4 {
@@ -33,11 +41,16 @@ struct FbTaskCard2x4 {
/* 🔴 v89 教训:ForEach key 必须带 version 前缀,否则下次推送不重绘 */
@LocalStorageProp('version') version: string = '';
/** 右半最多三条(数据侧已裁到 4,这里再兜底截断) */
/** 右半最多 TK_2X4_MAX 条(数据侧会裁;这里只兜底防御异常大数据)
*
* 🔴🔴 v95(用户 2026-10-26):「任务的 2x4 卡片,也是一样的。」
* —— 与观影 2×4 完全同一个病:`Column` 不能滚 + 硬截 3 条 ⇒ 第三条被压扁截断。
* ⇒ 容器改 `List`(可滚),上限提到 TK_2X4_MAX。详见 FbMovieCard2x4 的同名注释。
*/
private rows(): FbTaskRow4[] {
try {
const all: FbTaskRow4[] = JSON.parse(this.rowsJson) as FbTaskRow4[];
return all.slice(0, 3);
return all.slice(0, TK_2X4_MAX);
} catch (err) {
return [];
}
@@ -169,19 +182,25 @@ struct FbTaskCard2x4 {
.strokeWidth(0.5)
.color('#EFE6D8')
// ===== 右半:待办(不滚动)=====
// ===== 右半:待办(🔴 v95 改成可滚动)=====
Column() {
if (this.rows().length === 0) {
this.emptyHint()
} else {
Column({ space: 6 }) {
List({ space: 6 }) {
ForEach(this.rows(), (item: FbTaskRow4) => {
this.taskRow(item)
ListItem() {
this.taskRow(item)
}
.width('100%')
}, (item: FbTaskRow4) => `tk_${this.version}_${item.key}`)
}
.width('100%')
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
/* ⚠️ 与观影 2×4 同一套:layoutWeight 才有视口、Auto 才只在能滚时出条,
且**不要**加 justifyContent(Center)(内容超高时首行会被推出去)。 */
.scrollBar(BarState.Auto)
.cachedCount(4)
}
}
.layoutWeight(1)