/** * 家书 FamilyBook · 4×4 服务卡片(任务 · 上概述/新建,下滚动列表 + 卡上完成) * * 用户 2026-10-25:「任务卡片,也需要4x4和4x6,跟任务模块类似,列出所有任务, * 可以在卡片直接点击完成。有新建按钮。」 * ⇒ 上半:标题 + 更新时间 + 「+ 新建」(路由进 app 直开「新建任务」); * 下半:`List` 装全部待办(数据侧排序:逾期 > 进行中 > 普通), * 每行右侧一颗「✓ 完成」→ `postCardAction({action:'message', taskDone:id})` * → `FbFormAbility.markTaskDone()` 调接口 → 重算重推。 * * ⚠️ 卡片不能自己发网络请求,完成动作必须借道 FormExtensionAbility。 * ⚠️ ForEach key 带 version 前缀(v89 教训)。 */ let storageTkB = new LocalStorage(); /** 列表一行 */ class FbTaskRow9a { id: number = 0; key: string = ''; title: string = ''; dueText: string = ''; status: string = ''; boardColor: string = '#A8574F'; overdue: boolean = false; } @Entry(storageTkB) @Component struct FbTaskCard4x4 { @LocalStorageProp('state') state: string = 'loading'; @LocalStorageProp('hint') hint: string = ''; @LocalStorageProp('updatedAt') updatedAt: string = ''; @LocalStorageProp('openCount') openCount: number = 0; @LocalStorageProp('overdueCount') overdueCount: number = 0; @LocalStorageProp('allJson') allJson: string = '[]'; @LocalStorageProp('version') version: string = ''; private rows(): FbTaskRow9a[] { try { const all: FbTaskRow9a[] = JSON.parse(this.allJson) as FbTaskRow9a[]; return all; } catch (err) { return []; } } private openApp(): void { postCardAction(this, { action: 'router', abilityName: 'EntryAbility', params: { fbModule: 'tasks' } }); } /** 「+ 新建」→ 进 app 直开「新建任务」浮层(EntryAbility → modCreate 通道) */ private addOne(): void { postCardAction(this, { action: 'router', abilityName: 'EntryAbility', params: { fbModule: 'tasks', widgetAction: 'add' } }); } /** 卡上直接完成(id 必须转字符串:param 数值按 int32 传) */ private complete(item: FbTaskRow9a): void { postCardAction(this, { action: 'message', params: { taskDone: `${item.id}` } }); } /** 列表单行:色条 + 标题/截止 + ✓ 完成 */ @Builder listRow(item: FbTaskRow9a) { Row({ space: 8 }) { Column() .width(3) .height(34) .borderRadius(2) .backgroundColor(item.overdue ? '#C0533F' : item.boardColor) Column({ space: 2 }) { Text(item.title) .fontSize(13) .fontColor('#2C231D') .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) Row({ space: 4 }) { if (item.overdue) { Text('逾期') .fontSize(9) .fontColor('#C0533F') .maxLines(1) } if (item.dueText !== '') { Text(item.dueText) .fontSize(9) .fontColor(item.overdue ? '#C0533F' : '#A8988A') .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) .layoutWeight(1) } else { Text(item.status === 'doing' ? '进行中' : '待办') .fontSize(9) .fontColor('#A8988A') .maxLines(1) } } .width('100%') } .layoutWeight(1) .alignItems(HorizontalAlign.Start) /* ⭐ 卡上直接点完成(子组件 onClick 优先于整卡,不会连带进 app) */ Text('✓') .fontSize(14) .fontColor('#FFFFFF') .width(26) .height(26) .textAlign(TextAlign.Center) .borderRadius(13) .backgroundColor(item.overdue ? '#C0533F' : '#A8574F') .onClick((): void => this.complete(item)) } .alignItems(VerticalAlign.Center) .width('100%') .padding({ left: 8, right: 8, top: 5, bottom: 5 }) .borderRadius(8) .backgroundColor(item.overdue ? '#FBEDEA' : '#F7F2EC') } /** 空态 */ @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('#A8574F') .onClick((): void => this.addOne()) } .width('100%') // ===== 概述行 ===== Row({ space: 8 }) { Text(`待办 ${this.openCount} 条`) .fontSize(11) .fontColor('#A8574F') .fontWeight(FontWeight.Medium) Text(this.hint !== '' ? this.hint : (this.overdueCount > 0 ? '逾期 ' + this.overdueCount.toString() + ' 条' : '点 ✓ 直接完成')) .fontSize(10) .fontColor(this.hint !== '' || this.overdueCount > 0 ? '#C0533F' : '#A8988A') .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) .layoutWeight(1) } .width('100%') Divider() .strokeWidth(0.5) .color('#EFE6D8') // ===== 全部待办(可滚动,可直接完成)===== if (this.rows().length === 0) { this.emptyHint() } else { List({ space: 5 }) { ForEach(this.rows(), (item: FbTaskRow9a) => { ListItem() { this.listRow(item) } }, (item: FbTaskRow9a): string => `tl_${this.version}_${item.key}`) } .width('100%') .layoutWeight(1) .scrollBar(BarState.Off) } } .width('100%') .height('100%') .padding(12) .backgroundColor('#FFFFFF') .borderRadius(16) // ⭐ 整卡可点 .onClick((): void => this.openApp()) } }