diff --git a/entry/src/main/ets/common/Store.ets b/entry/src/main/ets/common/Store.ets index 8372a25..0858864 100644 --- a/entry/src/main/ets/common/Store.ets +++ b/entry/src/main/ets/common/Store.ets @@ -55,6 +55,17 @@ export class FbAppState { /** 跳转请求序号(页面 `@Monitor` 它;同一个日期连点两次也能再触发一次) */ @Trace calJumpSeq: number = 0; + /** + * 🔴 v91:非日程卡(观影 / 任务)上的「+ 新建」按钮走这条通道。 + * + * 与 `calJump*` 同一套三件套思路:key 记"哪个模块要开新建浮层", + * seq 自增触发热启动下页面的 `@Monitor`;页面用掉就把 key 清空 + * (否则用户关掉浮层后每次回该页又会自己弹出来,日程页踩过)。 + */ + @Trace modCreateKey: string = ''; + /** 新建请求序号(页面 `@Monitor` 它;连点两次也能再触发一次) */ + @Trace modCreateSeq: number = 0; + /** 是否宽屏(窗口短边 ≥ 600vp)—— 决定抽屉是「常驻侧栏」还是「滑出层」 */ @Trace wideLayout: boolean = false; diff --git a/entry/src/main/ets/data/FbCardMediaData.ets b/entry/src/main/ets/data/FbCardMediaData.ets index b1e1dd6..b6b9522 100644 --- a/entry/src/main/ets/data/FbCardMediaData.ets +++ b/entry/src/main/ets/data/FbCardMediaData.ets @@ -19,7 +19,8 @@ import { FB_SERVER_STORE } from './FbServerStore'; import { FB_TOKEN_STORE } from './FbTokenStore'; import { fbApi, FbApiResult, FbApiOptions } from './FbHttp'; import { fbCardBuild, FbCardData } from './FbCardData'; -import { parseTaskList } from './FbParse'; +import { parseTasks } from './FbParse'; +import { setTaskStatus } from './FbTaskRepo'; import { FbTaskItem } from '../model/Types'; const DOMAIN: number = 0x0000; @@ -85,6 +86,8 @@ export class FbMovieCardData { totalCount: number = 0; /** 最近观影(按日期倒序,最多 4 条) */ recentJson: string = '[]'; + /** 🔴 v91:4×4/6×4 的滚动列表(按日期倒序,最多 30 条)—— 4×2 的卡不吃这个字段 */ + listJson: string = '[]'; /** 🔴 v89 教训:ForEach key 前缀源 */ version: string = ''; } @@ -200,6 +203,15 @@ export async function fbMovieCardBuild(ctx: common.Context): Promise 进行中 > 普通,同组按截止时间升序),最多 4 条 */ + /** 逾期优先展示的几条(逾期 > 进行中 > 普通,同组按截止时间升序),最多 4 条 */ rowsJson: string = '[]'; + /** 🔴 v91:4×4/6×4 的滚动列表(同一排序,最多 30 条)—— 2×2/2×4 的卡不吃这个字段 */ + allJson: string = '[]'; version: string = ''; } @@ -247,6 +263,7 @@ function taskRank(t: FbTaskItem): number { /** 一条任务 → 卡片行 */ function taskRowOf(t: FbTaskItem): FbTaskCardRow { const r: FbTaskCardRow = new FbTaskCardRow(); + r.id = t.id; r.key = 't' + t.id.toString() + '_' + t.status; r.title = t.title !== '' ? t.title : '未命名任务'; r.dueText = t.dueText; @@ -259,8 +276,12 @@ function taskRowOf(t: FbTaskItem): FbTaskCardRow { /** * 组装任务卡数据(异步,**永远不抛错**)。 * - * ⚠️ `/api/v1/tasks` 默认视图就是"未完成任务"(已完成走 `?view=done`); - * 这里仍按 status 过滤一道,服务端口径将来变了也不会把 done 显示成待办。 + * 🔴🔴 v91 修「任务卡片没有数据」:`/api/v1/tasks` 默认视图回的是 + * **看板嵌套**结构 `{ok, data:{boards:[{name,color,tasks:[…]}]}}`, + * 原来用 `parseTaskList()`(只认扁平数组)⇒ 解析成 0 条,卡片永远空。 + * 改用 `parseTasks()` —— 它就是任务页看板视图用的那个扁平化解析 + * (顺带把**看板色**也填进每行,卡片色条不再全是兜底色)。 + * 完成态仍按 status 过滤一道,服务端口径将来变了也不会把 done 显示成待办。 */ export async function fbTaskCardBuild(ctx: common.Context): Promise { const out: FbTaskCardData = new FbTaskCardData(); @@ -290,7 +311,7 @@ export async function fbTaskCardBuild(ctx: common.Context): Promise { + const token: string = await cardToken(ctx); + if (token !== '' && FB_SERVER_STORE.get() !== '') { + try { + await setTaskStatus(token, id, 'done'); + } catch (err) { + hilog.warn(DOMAIN, TAG, '卡片完成任务 %{public}d 失败', id); + } + } + return fbTaskCardBuild(ctx); +} + /* ══════════════════════════════════════════════════════════════ 按卡片类型分流 ══════════════════════════════════════════════════════════════ */ diff --git a/entry/src/main/ets/entryability/EntryAbility.ets b/entry/src/main/ets/entryability/EntryAbility.ets index 7fcd9c0..a4e7cd9 100644 --- a/entry/src/main/ets/entryability/EntryAbility.ets +++ b/entry/src/main/ets/entryability/EntryAbility.ets @@ -190,8 +190,10 @@ export default class EntryAbility extends UIAbility { /* ---- ② 把"看哪一天"交给日程页 ---- 冷启动时日程页还没建,先写进全局状态(页面 `aboutToAppear` 会读); 热启动(应用已开着)走 `onNewWant`,页面不会重建 ⇒ 靠 `calJumpSeq` 自增 - 触发页面上的 `@Monitor`。两种入口共用同一份状态,不会"有时跳有时不跳"。 */ - if (jumpIso !== '' || wantCreate) { + 触发页面上的 `@Monitor`。两种入口共用同一份状态,不会"有时跳有时不跳"。 + 🔴 v91:`add` 动作只对**日程卡**走 calJump;观影/任务卡的「+」 + 走 `modCreate*` 通道(它们没有"看哪一天"的语义)。 */ + if (key === 'calendar' && (jumpIso !== '' || wantCreate)) { const st: FbAppState = fbAppState(); st.calJumpIso = jumpIso; st.calJumpCreate = wantCreate; @@ -199,6 +201,15 @@ export default class EntryAbility extends UIAbility { hilog.info(DOMAIN, 'FamilyBook', '卡片请求日程:%{public}s(新建=%{public}s)', jumpIso === '' ? '今天' : jumpIso, wantCreate ? '是' : '否'); } + /* ---- ③ v91:观影 / 任务卡上的「+ 新建」---- + 与 calJump 同一套:写全局状态(冷启动页面 aboutToAppear 读)+ + seq 自增(热启动触发 @Monitor)。页面用掉后自己清 key。 */ + if (wantCreate && (key === 'movies' || key === 'tasks')) { + const st: FbAppState = fbAppState(); + st.modCreateKey = key; + st.modCreateSeq = st.modCreateSeq + 1; + hilog.info(DOMAIN, 'FamilyBook', '卡片请求 %{public}s 新建', key); + } fbAppState().switchModule(key); hilog.info(DOMAIN, 'FamilyBook', '从卡片进入模块:%{public}s', key); } diff --git a/entry/src/main/ets/entryformability/FbFormAbility.ets b/entry/src/main/ets/entryformability/FbFormAbility.ets index 68c37c9..4f41e9d 100644 --- a/entry/src/main/ets/entryformability/FbFormAbility.ets +++ b/entry/src/main/ets/entryformability/FbFormAbility.ets @@ -24,7 +24,7 @@ import { FormExtensionAbility, formBindingData, formProvider, formInfo } from '@ import { Want } from '@kit.AbilityKit'; import { BusinessError } from '@kit.BasicServicesKit'; import { hilog } from '@kit.PerformanceAnalysisKit'; -import { fbCardDataByKind } from '../data/FbCardMediaData'; +import { fbCardDataByKind, fbTaskCardComplete, FbTaskCardData } from '../data/FbCardMediaData'; import { FB_CARD_FORMS } from '../data/FbCardForms'; import { FB_BG_SYNC } from '../common/FbBackgroundSync'; @@ -147,13 +147,49 @@ export default class FbFormAbility extends FormExtensionAbility { }); } + /** + * 🔴 v91:任务卡上点「完成」—— 把任务标成 done,重算后**推给所有任务卡**。 + * + * ⚠️ 推所有任务卡而不是只推这一张:还有别的任务卡(2×2 的待办数、别的 4×4) + * 也显示这条任务,只推一张会出现"这张没了、那张还挂着"。 + * ⚠️ message 里的 id 用正则抠:`{"taskDone":"12"}` / 嵌套 `{params:{…}}` + * 两种形态都能命中(与既有"子串匹配"取舍一致)。 + */ + private markTaskDone(formId: string, message: string): void { + const m: RegExpMatchArray | null = message.match(/"taskDone"\s*:\s*"?(\d+)/); + const id: number = m !== null ? parseInt(m[1], 10) : 0; + if (id <= 0 || isNaN(id)) { + hilog.warn(DOMAIN, TAG, '任务卡「完成」消息里没有可用 id:%{public}s', message); + return; + } + fbTaskCardComplete(this.context, id).then((data: FbTaskCardData): void => { + const binding: formBindingData.FormBindingData = formBindingData.createFormBindingData(data); + // 先回这张卡(用户手指点的那张,必须立刻看到变化) + formProvider.updateForm(formId, binding).catch((err: BusinessError): void => { + hilog.warn(DOMAIN, TAG, '任务卡 %{public}s 回推失败:%{public}s', formId, err.message); + }); + // 其余任务卡重算重推(数量不多,构建走同一份缓存口径) + FB_CARD_FORMS.pushAll(this.context).catch((err: BusinessError): void => { + hilog.warn(DOMAIN, TAG, '任务完成后全量重推失败:%{public}s', err.message); + }); + }).catch((err: BusinessError): void => { + hilog.error(DOMAIN, TAG, '完成任务 %{public}d 流程失败:%{public}s', id, err.message); + }); + } + onFormEvent(formId: string, message: string): void { /* 卡片上的按钮都走 `action: 'message'`,`message` 是 JSON 字符串。 ⚠️ 不解析 JSON:不同版本系统给的形态可能是 `{params:{...}}` 或直接 `{...}`, 子串匹配更稳(与 SyncCalendar 同一个取舍)。 ⚠️ v90:只有日程卡有 ‹ / › / 今 三个按钮;观影/任务卡不发 message 事件, - 收到也直接忽略(不把 message 撞进偏移量里)。 */ + 收到也直接忽略(不把 message 撞进偏移量里)。 + 🔴 v91:任务卡多了「完成」按钮(`taskDone`),**必须最先分流**—— + 它不认卡片类型也不认偏移量,认不出类型时也不能丢。 */ hilog.info(DOMAIN, TAG, '卡片事件:%{public}s %{public}s', formId, message); + if (message.indexOf('taskDone') >= 0) { + this.markTaskDone(formId, message); + return; + } FB_CARD_FORMS.kindOf(this.context, formId).then((kind: string): void => { if (kind !== 'calendar') { return; diff --git a/entry/src/main/ets/pages/card/FbMovieCard4x4.ets b/entry/src/main/ets/pages/card/FbMovieCard4x4.ets new file mode 100644 index 0000000..278a778 --- /dev/null +++ b/entry/src/main/ets/pages/card/FbMovieCard4x4.ets @@ -0,0 +1,186 @@ +/** + * 家书 FamilyBook · 4×4 服务卡片(观影 · 上概述/新建,下滚动列表) + * + * 用户 2026-10-25:「观影卡片,加4x4,和4x6,上面概述,加新建按钮, + * 下面瀑布流,就跟观影模块类似,可以滑下去看全部电影。」 + * ⇒ 上半:标题 + 更新时间 + 「+ 新建」(路由进 app 直开「记一场」); + * 下半:`List`(卡片**不支持 Scroll、只支持 List**,见 FbCard4x4 的注释) + * 装全部观影(数据侧倒序裁到 30 条),整卡可点进模块。 + * + * ⚠️ 卡片不能自己发网络请求:只画 `fbMovieCardBuild()` 推过来的字段。 + * ⚠️ ForEach key 带 version 前缀(v89 教训,不重绘就是它的锅)。 + */ + +let storageMvB = new LocalStorage(); + +/** 列表一行 */ +class FbMovieRow9a { + key: string = ''; + title: string = ''; + dateText: string = ''; + cinema: string = ''; + rating: string = ''; +} + +@Entry(storageMvB) +@Component +struct FbMovieCard4x4 { + @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(): FbMovieRow9a[] { + try { + const all: FbMovieRow9a[] = JSON.parse(this.listJson) as FbMovieRow9a[]; + 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' } + }); + } + + /** 列表单行:日期 + 片名/影院(+评分) */ + @Builder + listRow(item: FbMovieRow9a) { + Row({ space: 8 }) { + Text(item.dateText !== '' ? item.dateText : '—') + .fontSize(9) + .fontColor('#A8988A') + .width(46) + .maxLines(1) + Column({ space: 2 }) { + Text(item.title) + .fontSize(13) + .fontColor('#2C231D') + .maxLines(1) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + if (item.cinema !== '') { + Text(item.cinema) + .fontSize(9) + .fontColor('#A8988A') + .maxLines(1) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + } + } + .layoutWeight(1) + .alignItems(HorizontalAlign.Start) + if (item.rating !== '') { + Text(item.rating) + .fontSize(10) + .fontColor('#C08A3E') + .maxLines(1) + } + } + .alignItems(VerticalAlign.Center) + .width('100%') + .padding({ left: 8, right: 8, top: 5, bottom: 5 }) + .borderRadius(8) + .backgroundColor('#F6F2F7') + } + + /** 空态 */ + @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) + /* ⭐ 新建按钮(子组件 onClick 优先于整卡,不会连带进 app) */ + 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%') + + Divider() + .strokeWidth(0.5) + .color('#EFE6D8') + + // ===== 全部观影(可滚动)===== + if (this.rows().length === 0) { + this.emptyHint() + } else { + List({ space: 5 }) { + ForEach(this.rows(), (item: FbMovieRow9a) => { + ListItem() { + this.listRow(item) + } + }, (item: FbMovieRow9a): string => `ml_${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()) + } +} diff --git a/entry/src/main/ets/pages/card/FbMovieCard6x4.ets b/entry/src/main/ets/pages/card/FbMovieCard6x4.ets new file mode 100644 index 0000000..56e9304 --- /dev/null +++ b/entry/src/main/ets/pages/card/FbMovieCard6x4.ets @@ -0,0 +1,186 @@ +/** + * 家书 FamilyBook · 6×4 服务卡片(观影 · 上概述/新建,下滚动列表) + * + * 用户 2026-10-25:「观影卡片,加4x4,和4x6,上面概述,加新建按钮, + * 下面瀑布流,就跟观影模块类似,可以滑下去看全部电影。」 + * ⇒ 上半:标题 + 更新时间 + 「+ 新建」(路由进 app 直开「记一场」); + * 下半:`List`(卡片**不支持 Scroll、只支持 List**,见 FbCard4x4 的注释) + * 装全部观影(数据侧倒序裁到 30 条),整卡可点进模块。 + * + * ⚠️ 卡片不能自己发网络请求:只画 `fbMovieCardBuild()` 推过来的字段。 + * ⚠️ ForEach key 带 version 前缀(v89 教训,不重绘就是它的锅)。 + */ + +let storageMvB2 = new LocalStorage(); + +/** 列表一行 */ +class FbMovieRow9b { + key: string = ''; + title: string = ''; + dateText: string = ''; + cinema: string = ''; + rating: string = ''; +} + +@Entry(storageMvB2) +@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(): FbMovieRow9b[] { + try { + const all: FbMovieRow9b[] = JSON.parse(this.listJson) as FbMovieRow9b[]; + 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' } + }); + } + + /** 列表单行:日期 + 片名/影院(+评分) */ + @Builder + listRow(item: FbMovieRow9b) { + Row({ space: 8 }) { + Text(item.dateText !== '' ? item.dateText : '—') + .fontSize(9) + .fontColor('#A8988A') + .width(46) + .maxLines(1) + Column({ space: 2 }) { + Text(item.title) + .fontSize(13) + .fontColor('#2C231D') + .maxLines(1) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + if (item.cinema !== '') { + Text(item.cinema) + .fontSize(9) + .fontColor('#A8988A') + .maxLines(1) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + } + } + .layoutWeight(1) + .alignItems(HorizontalAlign.Start) + if (item.rating !== '') { + Text(item.rating) + .fontSize(10) + .fontColor('#C08A3E') + .maxLines(1) + } + } + .alignItems(VerticalAlign.Center) + .width('100%') + .padding({ left: 8, right: 8, top: 5, bottom: 5 }) + .borderRadius(8) + .backgroundColor('#F6F2F7') + } + + /** 空态 */ + @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) + /* ⭐ 新建按钮(子组件 onClick 优先于整卡,不会连带进 app) */ + 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%') + + Divider() + .strokeWidth(0.5) + .color('#EFE6D8') + + // ===== 全部观影(可滚动)===== + if (this.rows().length === 0) { + this.emptyHint() + } else { + List({ space: 5 }) { + ForEach(this.rows(), (item: FbMovieRow9b) => { + ListItem() { + this.listRow(item) + } + }, (item: FbMovieRow9b): string => `ml_${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()) + } +} diff --git a/entry/src/main/ets/pages/card/FbTaskCard4x4.ets b/entry/src/main/ets/pages/card/FbTaskCard4x4.ets new file mode 100644 index 0000000..b10078e --- /dev/null +++ b/entry/src/main/ets/pages/card/FbTaskCard4x4.ets @@ -0,0 +1,217 @@ +/** + * 家书 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()) + } +} diff --git a/entry/src/main/ets/pages/card/FbTaskCard6x4.ets b/entry/src/main/ets/pages/card/FbTaskCard6x4.ets new file mode 100644 index 0000000..a53c570 --- /dev/null +++ b/entry/src/main/ets/pages/card/FbTaskCard6x4.ets @@ -0,0 +1,217 @@ +/** + * 家书 FamilyBook · 6×4 服务卡片(任务 · 上概述/新建,下滚动列表 + 卡上完成) + * + * 用户 2026-10-25:「任务卡片,也需要4x4和4x6,跟任务模块类似,列出所有任务, + * 可以在卡片直接点击完成。有新建按钮。」 + * ⇒ 上半:标题 + 更新时间 + 「+ 新建」(路由进 app 直开「新建任务」); + * 下半:`List` 装全部待办(数据侧排序:逾期 > 进行中 > 普通), + * 每行右侧一颗「✓ 完成」→ `postCardAction({action:'message', taskDone:id})` + * → `FbFormAbility.markTaskDone()` 调接口 → 重算重推。 + * + * ⚠️ 卡片不能自己发网络请求,完成动作必须借道 FormExtensionAbility。 + * ⚠️ ForEach key 带 version 前缀(v89 教训)。 + */ + +let storageTkB2 = new LocalStorage(); + +/** 列表一行 */ +class FbTaskRow9b { + id: number = 0; + key: string = ''; + title: string = ''; + dueText: string = ''; + status: string = ''; + boardColor: string = '#A8574F'; + overdue: boolean = false; +} + +@Entry(storageTkB2) +@Component +struct FbTaskCard6x4 { + @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(): FbTaskRow9b[] { + try { + const all: FbTaskRow9b[] = JSON.parse(this.allJson) as FbTaskRow9b[]; + 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: FbTaskRow9b): void { + postCardAction(this, { + action: 'message', + params: { taskDone: `${item.id}` } + }); + } + + /** 列表单行:色条 + 标题/截止 + ✓ 完成 */ + @Builder + listRow(item: FbTaskRow9b) { + 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: FbTaskRow9b) => { + ListItem() { + this.listRow(item) + } + }, (item: FbTaskRow9b): 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()) + } +} diff --git a/entry/src/main/ets/pages/movies/FbMoviesPage.ets b/entry/src/main/ets/pages/movies/FbMoviesPage.ets index 678a591..e271c20 100644 --- a/entry/src/main/ets/pages/movies/FbMoviesPage.ets +++ b/entry/src/main/ets/pages/movies/FbMoviesPage.ets @@ -211,6 +211,24 @@ export struct FbMoviesPage { aboutToAppear(): void { FB_BACK.register('movies', (): boolean => this.backOnce()); this.bootstrap(); + /* 🔴 v91:桌面观影卡的「+ 新建」→ 冷启动直接开「记一场」浮层 + (热启动走下面的 @Monitor)。用掉就清 key:否则用户关掉浮层后 + 每次回本页都会自己弹出来(日程页踩过)。 */ + this.applyCardAdd(); + } + + /** 消费卡片带来的"要开新建浮层"意图(有则用掉并打开) */ + private applyCardAdd(): void { + if (fbAppState().modCreateKey === 'movies') { + fbAppState().modCreateKey = ''; + this.newOpen = true; + } + } + + /** 卡片在应用已开着时被点(`onNewWant` 改了全局状态)→ 这里响应 */ + @Monitor('app.modCreateSeq') + onCardAdd(): void { + this.applyCardAdd(); } /** 该电影的海报是否已确认加载失败 */ diff --git a/entry/src/main/ets/pages/tasks/FbTasksPage.ets b/entry/src/main/ets/pages/tasks/FbTasksPage.ets index 54a44b6..56aa179 100644 --- a/entry/src/main/ets/pages/tasks/FbTasksPage.ets +++ b/entry/src/main/ets/pages/tasks/FbTasksPage.ets @@ -148,6 +148,24 @@ export struct FbTasksPage { FB_BACK.register('tasks', (): boolean => this.backOnce()); this.load(); this.loadMeta(); + /* 🔴 v91:桌面任务卡的「+ 新建」→ 冷启动直接开「新建任务」浮层 + (热启动走下面的 @Monitor)。用掉就清 key:否则用户关掉浮层后 + 每次回本页都会自己弹出来(日程页踩过)。 */ + this.applyCardAdd(); + } + + /** 消费卡片带来的"要开新建浮层"意图(有则用掉并打开) */ + private applyCardAdd(): void { + if (fbAppState().modCreateKey === 'tasks') { + fbAppState().modCreateKey = ''; + this.openNew(); + } + } + + /** 卡片在应用已开着时被点(`onNewWant` 改了全局状态)→ 这里响应 */ + @Monitor('app.modCreateSeq') + onCardAdd(): void { + this.applyCardAdd(); } /** 拉全部看板 + 分类(筛选条、编辑表单、面板的"挪分组"共用一份) */ diff --git a/entry/src/main/resources/base/element/string.json b/entry/src/main/resources/base/element/string.json index d42820a..8c6799f 100644 --- a/entry/src/main/resources/base/element/string.json +++ b/entry/src/main/resources/base/element/string.json @@ -51,6 +51,22 @@ { "name": "card_task_2x4_name", "value": "家书 · 待办任务" + }, + { + "name": "card_movie_4x4_name", + "value": "家书 · 观影(全部)" + }, + { + "name": "card_movie_6x4_name", + "value": "家书 · 观影(宽)" + }, + { + "name": "card_task_4x4_name", + "value": "家书 · 待办(全部)" + }, + { + "name": "card_task_6x4_name", + "value": "家书 · 待办(宽)" } ] } diff --git a/entry/src/main/resources/base/profile/form_config.json b/entry/src/main/resources/base/profile/form_config.json index 0ac2bfa..947603a 100644 --- a/entry/src/main/resources/base/profile/form_config.json +++ b/entry/src/main/resources/base/profile/form_config.json @@ -135,6 +135,74 @@ "updateDuration": 1, "defaultDimension": "2*4", "supportDimensions": ["2*4"] + }, + { + "name": "FbMovieCard4x4", + "displayName": "$string:card_movie_4x4_name", + "description": "$string:card_desc", + "src": "./ets/pages/card/FbMovieCard4x4.ets", + "window": { + "designWidth": 720, + "autoDesignWidth": true + }, + "colorMode": "light", + "uiSyntax": "arkts", + "isDefault": false, + "updateEnabled": true, + "updateDuration": 1, + "defaultDimension": "4*4", + "supportDimensions": ["4*4"] + }, + { + "name": "FbMovieCard6x4", + "displayName": "$string:card_movie_6x4_name", + "description": "$string:card_desc", + "src": "./ets/pages/card/FbMovieCard6x4.ets", + "window": { + "designWidth": 720, + "autoDesignWidth": true + }, + "colorMode": "light", + "uiSyntax": "arkts", + "isDefault": false, + "updateEnabled": true, + "updateDuration": 1, + "defaultDimension": "6*4", + "supportDimensions": ["6*4"] + }, + { + "name": "FbTaskCard4x4", + "displayName": "$string:card_task_4x4_name", + "description": "$string:card_desc", + "src": "./ets/pages/card/FbTaskCard4x4.ets", + "window": { + "designWidth": 720, + "autoDesignWidth": true + }, + "colorMode": "light", + "uiSyntax": "arkts", + "isDefault": false, + "updateEnabled": true, + "updateDuration": 1, + "defaultDimension": "4*4", + "supportDimensions": ["4*4"] + }, + { + "name": "FbTaskCard6x4", + "displayName": "$string:card_task_6x4_name", + "description": "$string:card_desc", + "src": "./ets/pages/card/FbTaskCard6x4.ets", + "window": { + "designWidth": 720, + "autoDesignWidth": true + }, + "colorMode": "light", + "uiSyntax": "arkts", + "isDefault": false, + "updateEnabled": true, + "updateDuration": 1, + "defaultDimension": "6*4", + "supportDimensions": ["6*4"] } ] }