diff --git a/entry/src/main/ets/data/FbCardForms.ets b/entry/src/main/ets/data/FbCardForms.ets index b84ff92..4079635 100644 --- a/entry/src/main/ets/data/FbCardForms.ets +++ b/entry/src/main/ets/data/FbCardForms.ets @@ -53,7 +53,7 @@ import { preferences } from '@kit.ArkData'; import { formBindingData, formProvider } from '@kit.FormKit'; import { hilog } from '@kit.PerformanceAnalysisKit'; import { BusinessError } from '@kit.BasicServicesKit'; -import { FbCardData, fbCardBuild } from './FbCardData'; +import { fbCardDataByKind } from './FbCardMediaData'; const DOMAIN: number = 0x0000; const TAG: string = 'FbCardForms'; @@ -61,6 +61,10 @@ const TAG: string = 'FbCardForms'; const STORE_NAME: string = 'fb_card_forms'; const K_IDS: string = 'form_ids'; const K_OFF: string = 'day_offsets'; +/** 🔴 v90:formId → 卡片类型(`calendar` / `movies` / `tasks`)。 + * ⚠️ 旧卡(v90 之前登记的日程卡)没有这一项 ⇒ 读不到一律按 `calendar` 处理, + * 绝不能让升级把桌面上已有的日程卡弄瞎。 */ +const K_KIND: string = 'form_kinds'; /** 翻页最多到前后一年(防外部输入把日期算成 Invalid Date) */ const MAX_OFFSET: number = 366; @@ -181,6 +185,52 @@ class FbCardFormsImpl { } } + /* ---- 卡片类型(v90:formId → calendar / movies / tasks)---- + 结构与偏移量同构(键值对都落在一份字符串里),值是类型名而非数字。 */ + + private parseKinds(raw: string): Map { + const map: Map = new Map(); + for (const item of raw.split(SEP_ITEM)) { + if (item === '') { + continue; + } + const at: number = item.indexOf(SEP_KV); + if (at <= 0) { + continue; + } + map.set(item.substring(0, at), item.substring(at + 1)); + } + return map; + } + + private joinKinds(map: Map): string { + const parts: string[] = []; + map.forEach((kind: string, id: string): void => { + parts.push(id + SEP_KV + kind); + }); + return parts.join(SEP_ITEM); + } + + /** 读某张卡的类型(读不到按 `calendar` —— 兼容 v90 之前登记的旧日程卡) */ + async kindOf(ctx: common.Context, formId: string): Promise { + const map: Map = this.parseKinds(await this.readStr(ctx, K_KIND)); + const v: string | undefined = map.get(formId); + return v === undefined || v === '' ? 'calendar' : v; + } + + private async setKind(ctx: common.Context, formId: string, kind: string): Promise { + const map: Map = this.parseKinds(await this.readStr(ctx, K_KIND)); + map.set(formId, kind); + await this.writeStr(ctx, K_KIND, this.joinKinds(map)); + } + + private async clearKind(ctx: common.Context, formId: string): Promise { + const map: Map = this.parseKinds(await this.readStr(ctx, K_KIND)); + if (map.delete(formId)) { + await this.writeStr(ctx, K_KIND, this.joinKinds(map)); + } + } + /* ---- 卡片注册表 ---- */ private async ids(ctx: common.Context): Promise { @@ -198,7 +248,7 @@ class FbCardFormsImpl { await this.writeStr(ctx, K_IDS, list.join(SEP_ITEM)); } - async register(ctx: common.Context, formId: string): Promise { + async register(ctx: common.Context, formId: string, kind: string): Promise { if (formId === '') { return; } @@ -207,17 +257,21 @@ class FbCardFormsImpl { list.push(formId); await this.saveIds(ctx, list); } + // 类型只增不改:同一张卡不会换类型(卡片类型由 form_config 决定,随 formId 固定) + await this.setKind(ctx, formId, kind); } async unregister(ctx: common.Context, formId: string): Promise { const list: string[] = (await this.ids(ctx)).filter((id: string): boolean => id !== formId); await this.saveIds(ctx, list); + await this.clearKind(ctx, formId); } /** - * 应用内日程变化后刷新**所有**已添加的卡。 + * 应用内数据变化后刷新**所有**已添加的卡。 * - * ⚠️ 按偏移量分组构建:两张卡可能翻在不同日期,逐张构建会重复打同一个接口。 + * ⚠️ 按类型 + 偏移量分组构建:两张卡可能类型不同 / 翻在不同日期, + * 逐张构建会重复打同一个接口。 * ⚠️ 推失败的 formId 直接**注销**(卡片多半已被用户删掉),否则每刷一次都白推一遍。 */ async pushAll(ctx: common.Context): Promise { @@ -233,16 +287,20 @@ class FbCardFormsImpl { hilog.warn(DOMAIN, TAG, 'pushAll:注册表为空(桌面没有卡片,或跨进程没读到)'); return; } - const cache: Map = - new Map(); + const cache: Map = + new Map(); const stale: string[] = []; for (const formId of list) { + /* 🔴 v90:每张卡按自己的类型构建(日程卡才吃偏移量; + 观影/任务卡没有"看哪一天"的概念,偏移量传 0 即可)。 */ + const kind: string = await this.kindOf(ctx, formId); const off: number = await this.getOffset(ctx, formId); - let binding: formBindingData.FormBindingData | undefined = cache.get(off); + const ck: string = kind + '#' + off.toString(); + let binding: formBindingData.FormBindingData | undefined = cache.get(ck); if (binding === undefined) { - const data: FbCardData = await fbCardBuild(ctx, off); + const data: Object = await fbCardDataByKind(ctx, kind, off); binding = formBindingData.createFormBindingData(data); - cache.set(off, binding); + cache.set(ck, binding); } try { await formProvider.updateForm(formId, binding); @@ -252,7 +310,7 @@ class FbCardFormsImpl { stale.push(formId); } } - hilog.info(DOMAIN, TAG, 'pushAll:已推 %{public}d 张卡(%{public}d 种偏移)', + hilog.info(DOMAIN, TAG, 'pushAll:已推 %{public}d 张卡(%{public}d 种组合)', list.length, cache.size); if (stale.length > 0) { const keep: string[] = list.filter((id: string): boolean => stale.indexOf(id) < 0); diff --git a/entry/src/main/ets/data/FbCardMediaData.ets b/entry/src/main/ets/data/FbCardMediaData.ets new file mode 100644 index 0000000..b1e1dd6 --- /dev/null +++ b/entry/src/main/ets/data/FbCardMediaData.ets @@ -0,0 +1,366 @@ +/** + * 家书 FamilyBook · **观影 / 任务**桌面卡片数据(鸿蒙端)—— v90 + * + * 用户 2026-10-24:「在做几个服务卡片,包括观影、任务,你看着做吧。」 + * ⇒ 照日程卡片(`FbCardData`)的既有范式: + * · FormExtensionAbility 回调只活约 10 秒 ⇒ 永远不抛错、立刻给壳; + * · `FormBindingData` 只认标量 ⇒ 列表一律 `JSON.stringify`; + * · 卡片是后台刷新 ⇒ `authProbe: true` 且显式带 token(401 不踢登录); + * · 🔴 v89 教训:卡片 ForEach 的 key **必须**带 `version`(`Date.now()`)前缀, + * 否则下次推送数据换了一批、key 却一样 ⇒ ArkUI 直接复用旧组件不重绘。 + * + * 与日程卡的一个差异:这两类卡**没有翻页**(观影/任务没有"看哪一天"的概念), + * `dayOffset` 对它们无意义 ⇒ 构建时不吃偏移,`FbFormAbility` 按类型分流即可。 + */ + +import { common } from '@kit.AbilityKit'; +import { hilog } from '@kit.PerformanceAnalysisKit'; +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 { FbTaskItem } from '../model/Types'; + +const DOMAIN: number = 0x0000; +const TAG: string = 'FbCardMedia'; + +/** 两位补零(与 FbCardData 同口径,卡片时间戳用) */ +function pad2(v: number): string { + return v < 10 ? '0' + v.toString() : v.toString(); +} + +/** 当前时刻 `HH:mm` */ +function clockText(): string { + const d: Date = new Date(); + return pad2(d.getHours()) + ':' + pad2(d.getMinutes()); +} + +/** `str | null | undefined` → `str`(服务端行里 null 很常见) */ +function strOf(v: string | null | undefined): string { + return typeof v === 'string' ? v : ''; +} + +/** `number | null | undefined` → `number` */ +function numOf(v: number | null | undefined): number { + return typeof v === 'number' && !isNaN(v) ? v : 0; +} + +/** 卡片环境预加载 + 取令牌(三个动作一起做,失败给空串) */ +async function cardToken(ctx: common.Context): Promise { + try { + await FB_SERVER_STORE.init(ctx); + await FB_TOKEN_STORE.init(ctx); + } catch (err) { + hilog.warn(DOMAIN, TAG, '卡片环境预加载失败:%{public}s', JSON.stringify(err)); + } + return FB_TOKEN_STORE.get(); +} + +/* ══════════════════════════════════════════════════════════════ + 观影卡 + ══════════════════════════════════════════════════════════════ */ + +/** 观影卡上的一行(最近观影) */ +export class FbMovieCardRow { + key: string = ''; + title: string = ''; + /** `10月24日`(解析不出就空着,卡片不画日期) */ + dateText: string = ''; + cinema: string = ''; + /** 家庭评分 `★ 4.5`(没评分为空串) */ + rating: string = ''; +} + +/** 观影卡整体数据(字段名 = 卡片页 `@LocalStorageProp` 键名,两侧改名必须同步) */ +export class FbMovieCardData { + /** `ok` / `login` / `noserver` / `offline` / `loading` */ + state: string = 'loading'; + hint: string = ''; + /** `15:40 更新` */ + updatedAt: string = ''; + /** 今年看过的场数(2×2 的大数字) */ + yearCount: number = 0; + /** 本次拉到的总场数(服务端分页最多 200 条,够用) */ + totalCount: number = 0; + /** 最近观影(按日期倒序,最多 4 条) */ + recentJson: string = '[]'; + /** 🔴 v89 教训:ForEach key 前缀源 */ + version: string = ''; +} + +/** 服务端观影列表的原始行(只取卡片要用的几个字段) */ +interface FbRawMovie { + id?: number | null; + title?: string | null; + starts_at?: string | null; + cinema_name?: string | null; + review_count?: number | null; + review_avg?: number | null; +} + +/** `starts_at` → `10月24日`(解析不出给空串) */ +function movieDateText(startsAt: string): string { + // `YYYY-MM-DD HH:MM:SS` + if (startsAt.length < 10) { + return ''; + } + const mm: string = startsAt.substring(5, 7); + const dd: string = startsAt.substring(8, 10); + const m: number = parseInt(mm, 10); + const d: number = parseInt(dd, 10); + if (isNaN(m) || isNaN(d) || m < 1 || m > 12 || d < 1 || d > 31) { + return ''; + } + return m.toString() + '月' + d.toString() + '日'; +} + +/** 一条原始行 → 卡片行 */ +function movieRowOf(m: FbRawMovie, idx: number): FbMovieCardRow { + const r: FbMovieCardRow = new FbMovieCardRow(); + const id: number = numOf(m.id); + r.key = 'm' + idx.toString() + '_' + id.toString(); + r.title = strOf(m.title) !== '' ? strOf(m.title) : '未命名观影'; + r.dateText = movieDateText(strOf(m.starts_at)); + r.cinema = strOf(m.cinema_name); + const avg: number = numOf(m.review_avg); + r.rating = avg > 0 ? '★ ' + avg.toFixed(1) : ''; + return r; +} + +/** + * 组装观影卡数据(异步,**永远不抛错**)。 + * + * ⚠️ 直接拉 `/api/v1/movies`(与同步同一条接口,per_page=200 一次拿全): + * 卡片进程不能也不该走应用里的本地库(FbDb 是应用进程的单例)。 + */ +export async function fbMovieCardBuild(ctx: common.Context): Promise { + const out: FbMovieCardData = new FbMovieCardData(); + out.updatedAt = clockText(); + out.version = Date.now().toString(); + + const token: string = await cardToken(ctx); + if (FB_SERVER_STORE.get() === '') { + out.state = 'noserver'; + out.hint = '先在「家书」里填写服务器地址'; + return out; + } + if (token === '') { + out.state = 'login'; + out.hint = '先在「家书」里登录'; + return out; + } + + const query: Record = {}; + query['per_page'] = '200'; + query['page'] = '1'; + const opts: FbApiOptions = { method: 'GET', query: query, token: token, authProbe: true }; + const r: FbApiResult = await fbApi('/api/v1/movies', opts); + if (!r.ok) { + out.state = 'offline'; + out.hint = r.status === 401 ? '登录已失效,请重新登录' + : (r.error !== '' ? r.error : '连不上服务器'); + return out; + } + + // 解析(解析失败按"暂时没数据"处理,不抛) + let arr: FbRawMovie[] = []; + try { + arr = JSON.parse(r.data) as FbRawMovie[]; + } catch (err) { + hilog.warn(DOMAIN, TAG, '观影卡数据解析失败'); + } + if (!Array.isArray(arr)) { + arr = []; + } + + // 按观看日期倒序(服务端顺序不保证,卡片要"最近几部"就必须自己排; + // ISO 日期字符串可直接按字典序比较) + arr.sort((a: FbRawMovie, b: FbRawMovie): number => { + const sa: string = strOf(a.starts_at); + const sb: string = strOf(b.starts_at); + if (sa === sb) { + return 0; + } + return sa > sb ? -1 : 1; + }); + + const nowYear: string = (new Date()).getFullYear().toString(); + let yearCount: number = 0; + for (const m of arr) { + if (strOf(m.starts_at).substring(0, 4) === nowYear) { + yearCount++; + } + } + out.yearCount = yearCount; + out.totalCount = arr.length; + + const rows: FbMovieCardRow[] = []; + for (let i = 0; i < arr.length && rows.length < 4; i++) { + rows.push(movieRowOf(arr[i], i)); + } + out.recentJson = JSON.stringify(rows); + out.state = 'ok'; + out.hint = ''; + return out; +} + +/* ══════════════════════════════════════════════════════════════ + 任务卡 + ══════════════════════════════════════════════════════════════ */ + +/** 任务卡上的一行 */ +export class FbTaskCardRow { + key: string = ''; + title: string = ''; + /** 截止时间文案(服务端给的显示文本,空 = 没有期限) */ + dueText: string = ''; + /** `todo` / `doing`(done 的不进卡片) */ + status: string = ''; + /** 看板主题色(左侧色条) */ + boardColor: string = '#A8574F'; + overdue: boolean = false; +} + +/** 任务卡整体数据 */ +export class FbTaskCardData { + state: string = 'loading'; + hint: string = ''; + updatedAt: string = ''; + /** 未完成任务数(todo + doing) */ + openCount: number = 0; + /** 其中逾期数(标题行给红色提示) */ + overdueCount: number = 0; + /** 优先展示的几条(逾期 > 进行中 > 普通,同组按截止时间升序),最多 4 条 */ + rowsJson: string = '[]'; + version: string = ''; +} + +/** 卡片展示优先级:逾期 0 / 进行中 1 / 普通 2 */ +function taskRank(t: FbTaskItem): number { + if (t.overdue) { + return 0; + } + return t.status === 'doing' ? 1 : 2; +} + +/** 一条任务 → 卡片行 */ +function taskRowOf(t: FbTaskItem): FbTaskCardRow { + const r: FbTaskCardRow = new FbTaskCardRow(); + r.key = 't' + t.id.toString() + '_' + t.status; + r.title = t.title !== '' ? t.title : '未命名任务'; + r.dueText = t.dueText; + r.status = t.status; + r.boardColor = t.boardColor !== '' ? t.boardColor : '#A8574F'; + r.overdue = t.overdue; + return r; +} + +/** + * 组装任务卡数据(异步,**永远不抛错**)。 + * + * ⚠️ `/api/v1/tasks` 默认视图就是"未完成任务"(已完成走 `?view=done`); + * 这里仍按 status 过滤一道,服务端口径将来变了也不会把 done 显示成待办。 + */ +export async function fbTaskCardBuild(ctx: common.Context): Promise { + const out: FbTaskCardData = new FbTaskCardData(); + out.updatedAt = clockText(); + out.version = Date.now().toString(); + + const token: string = await cardToken(ctx); + if (FB_SERVER_STORE.get() === '') { + out.state = 'noserver'; + out.hint = '先在「家书」里填写服务器地址'; + return out; + } + if (token === '') { + out.state = 'login'; + out.hint = '先在「家书」里登录'; + return out; + } + + const opts: FbApiOptions = { method: 'GET', token: token, authProbe: true }; + const r: FbApiResult = await fbApi('/api/v1/tasks', opts); + if (!r.ok) { + out.state = 'offline'; + out.hint = r.status === 401 ? '登录已失效,请重新登录' + : (r.error !== '' ? r.error : '连不上服务器'); + return out; + } + + let items: FbTaskItem[] = []; + try { + items = parseTaskList(r.data); + } catch (err) { + hilog.warn(DOMAIN, TAG, '任务卡数据解析失败'); + } + + const open: FbTaskItem[] = []; + for (const t of items) { + if (t.status !== 'done') { + open.push(t); + } + } + out.openCount = open.length; + + let overdueCount: number = 0; + for (const t of open) { + if (t.overdue) { + overdueCount++; + } + } + out.overdueCount = overdueCount; + + // 逾期优先,其次进行中,再普通;同组按截止时间升序(没有期限的排最后) + open.sort((a: FbTaskItem, b: FbTaskItem): number => { + const ra: number = taskRank(a); + const rb: number = taskRank(b); + if (ra !== rb) { + return ra - rb; + } + if (a.dueAt === b.dueAt) { + return 0; + } + if (a.dueAt === '') { + return 1; + } + if (b.dueAt === '') { + return -1; + } + return a.dueAt < b.dueAt ? -1 : 1; + }); + + const rows: FbTaskCardRow[] = []; + for (let i = 0; i < open.length && rows.length < 4; i++) { + rows.push(taskRowOf(open[i])); + } + out.rowsJson = JSON.stringify(rows); + out.state = 'ok'; + out.hint = ''; + return out; +} + +/* ══════════════════════════════════════════════════════════════ + 按卡片类型分流 + ══════════════════════════════════════════════════════════════ */ + +/** + * 按卡片类型构建数据 —— `FbFormAbility` 与 `FbCardForms.pushAll` 共用这一个入口。 + * + * @param kind `calendar` / `movies` / `tasks`(认不出按日程处理 —— + * 兼容升级前已登记的旧卡片,它们的 kind 没落盘,只能默认日程) + * @param dayOffset 仅日程卡用(翻页);其余类型忽略 + */ +export async function fbCardDataByKind(ctx: common.Context, kind: string, + dayOffset: number): Promise { + if (kind === 'movies') { + const m: FbMovieCardData = await fbMovieCardBuild(ctx); + return m; + } + if (kind === 'tasks') { + const t: FbTaskCardData = await fbTaskCardBuild(ctx); + return t; + } + const d: FbCardData = await fbCardBuild(ctx, dayOffset); + return d; +} diff --git a/entry/src/main/ets/entryformability/FbFormAbility.ets b/entry/src/main/ets/entryformability/FbFormAbility.ets index 8dcc286..68c37c9 100644 --- a/entry/src/main/ets/entryformability/FbFormAbility.ets +++ b/entry/src/main/ets/entryformability/FbFormAbility.ets @@ -20,29 +20,43 @@ * 系统定时刷新**不能**把用户翻走的日期拽回今天。 */ -import { FormExtensionAbility, formBindingData, formProvider } from '@kit.FormKit'; +import { FormExtensionAbility, formBindingData, formProvider, formInfo } from '@kit.FormKit'; import { Want } from '@kit.AbilityKit'; import { BusinessError } from '@kit.BasicServicesKit'; import { hilog } from '@kit.PerformanceAnalysisKit'; -import { FbCardData, fbCardBuild } from '../data/FbCardData'; +import { fbCardDataByKind } from '../data/FbCardMediaData'; import { FB_CARD_FORMS } from '../data/FbCardForms'; import { FB_BG_SYNC } from '../common/FbBackgroundSync'; const DOMAIN: number = 0x0000; const TAG: string = 'FbForm'; +/** 卡片名 → 数据类型(v90 起不止日程卡)。 + * ⚠️ 名字来自 `form_config.json` 的 `name` 字段(Want 里的 `form_name`), + * 两边改名必须同步 —— 改一边的表现是"新卡永远拿日程数据",编译不报错。 */ +function kindOfFormName(name: string): string { + if (name.indexOf('FbMovieCard') === 0) { + return 'movies'; + } + if (name.indexOf('FbTaskCard') === 0) { + return 'tasks'; + } + return 'calendar'; +} + export default class FbFormAbility extends FormExtensionAbility { /** * 重新算一遍并推给指定卡片。 * - * ⚠️ 四块卡(2×2 / 2×4 / 4×4 / 6×4)共用这份数据:它们只是**排版不同**, - * 数字与条目必须是同一个 —— 多算一遍既慢又可能口径不一致。 + * ⚠️ v90 起按类型分流:日程卡吃 `dayOffset`(翻页); + * 观影/任务卡没有"看哪一天"的概念,`dayOffset` 固定传 0。 */ - private push(formId: string, dayOffset: number): void { - fbCardBuild(this.context, dayOffset).then((data: FbCardData): void => { + private push(formId: string, kind: string, dayOffset: number): void { + fbCardDataByKind(this.context, kind, dayOffset).then((data: Object): void => { const binding: formBindingData.FormBindingData = formBindingData.createFormBindingData(data); formProvider.updateForm(formId, binding).then((): void => { - hilog.info(DOMAIN, TAG, '卡片 %{public}s 已更新(偏移 %{public}d)', formId, data.dayOffset); + hilog.info(DOMAIN, TAG, '卡片 %{public}s(%{public}s) 已更新(偏移 %{public}d)', + formId, kind, dayOffset); }).catch((err: BusinessError): void => { // 卡片可能已经被用户删了 —— 打一条日志就够,不要重试 hilog.warn(DOMAIN, TAG, '卡片 %{public}s 更新失败:%{public}s', formId, err.message); @@ -61,21 +75,31 @@ export default class FbFormAbility extends FormExtensionAbility { return typeof raw === 'string' ? raw : ''; } + /** 从 Want 里取卡片名(`ohos.extra.param.key.form_name`,即 form_config 的 name) */ + private formNameOf(want: Want): string { + if (want.parameters === undefined) { + return ''; + } + const raw: Object | undefined = want.parameters[formInfo.FormParam.NAME_KEY]; + return typeof raw === 'string' ? raw : ''; + } + onAddForm(want: Want): formBindingData.FormBindingData { const formId: string = this.formIdOf(want); - hilog.info(DOMAIN, TAG, '添加卡片:%{public}s', formId); + const kind: string = kindOfFormName(this.formNameOf(want)); + hilog.info(DOMAIN, TAG, '添加卡片:%{public}s(%{public}s)', formId, kind); if (formId !== '') { // 异步:登记 + 补真实数据(不 await:见文件头约束 ①) - FB_CARD_FORMS.register(this.context, formId).then((): void => { - this.push(formId, 0); + FB_CARD_FORMS.register(this.context, formId, kind).then((): void => { + this.push(formId, kind, 0); }).catch((err: BusinessError): void => { hilog.warn(DOMAIN, TAG, '卡片登记失败:%{public}s', err.message); }); } // 先给一份"正在取"的壳 —— 卡片上会显示空态,而不是白屏 - const shell: FbCardData = new FbCardData(); - shell.state = 'loading'; - shell.hint = '正在取数据…'; + const shell: Record = {}; + shell['state'] = 'loading'; + shell['hint'] = '正在取数据…'; return formBindingData.createFormBindingData(shell); } @@ -100,11 +124,21 @@ export default class FbFormAbility extends FormExtensionAbility { ⚠️ 节流:系统刷新可能漂移或重入,`runThrottledByCard` 内部用时间戳挡了一道, 并且**先写时间戳再跑**,避免卡片进程与主进程并发拉同一批数据。 */ // ⚠️ 必须按**这张卡自己的偏移**重建,否则定时刷新会把用户翻走的日期拽回今天 - FB_CARD_FORMS.getOffset(this.context, formId).then((off: number): void => { - this.push(formId, off); + // 🔴 v90:偏移量只对日程卡有意义;观影/任务卡按类型直接推。 + FB_CARD_FORMS.kindOf(this.context, formId).then((kind: string): void => { + if (kind !== 'calendar') { + this.push(formId, kind, 0); + return; + } + FB_CARD_FORMS.getOffset(this.context, formId).then((off: number): void => { + this.push(formId, kind, off); + }).catch((err: BusinessError): void => { + hilog.warn(DOMAIN, TAG, '读卡片偏移失败:%{public}s', err.message); + this.push(formId, kind, 0); + }); }).catch((err: BusinessError): void => { - hilog.warn(DOMAIN, TAG, '读卡片偏移失败:%{public}s', err.message); - this.push(formId, 0); + hilog.warn(DOMAIN, TAG, '读卡片类型失败(按日程处理):%{public}s', err.message); + this.push(formId, 'calendar', 0); }); /* ⚠️ 这一句必须放在**推送之后**(原因见上面的 ①:卡片刷新只有几秒的窗口, 不能被 12 个模块的同步挤掉)。两条都是"发起即返回",不会互相阻塞。 */ @@ -116,26 +150,35 @@ export default class FbFormAbility extends FormExtensionAbility { onFormEvent(formId: string, message: string): void { /* 卡片上的按钮都走 `action: 'message'`,`message` 是 JSON 字符串。 ⚠️ 不解析 JSON:不同版本系统给的形态可能是 `{params:{...}}` 或直接 `{...}`, - 子串匹配更稳(与 SyncCalendar 同一个取舍)。 */ + 子串匹配更稳(与 SyncCalendar 同一个取舍)。 + ⚠️ v90:只有日程卡有 ‹ / › / 今 三个按钮;观影/任务卡不发 message 事件, + 收到也直接忽略(不把 message 撞进偏移量里)。 */ hilog.info(DOMAIN, TAG, '卡片事件:%{public}s %{public}s', formId, message); - FB_CARD_FORMS.getOffset(this.context, formId).then((cur: number): void => { - let next: number = cur; - if (message.indexOf('prev') >= 0) { - next = cur - 1; - } else if (message.indexOf('next') >= 0) { - next = cur + 1; - } else if (message.indexOf('today') >= 0) { - next = 0; + FB_CARD_FORMS.kindOf(this.context, formId).then((kind: string): void => { + if (kind !== 'calendar') { + return; } - FB_CARD_FORMS.setOffset(this.context, formId, next).then((off: number): void => { - this.push(formId, off); + FB_CARD_FORMS.getOffset(this.context, formId).then((cur: number): void => { + let next: number = cur; + if (message.indexOf('prev') >= 0) { + next = cur - 1; + } else if (message.indexOf('next') >= 0) { + next = cur + 1; + } else if (message.indexOf('today') >= 0) { + next = 0; + } + FB_CARD_FORMS.setOffset(this.context, formId, next).then((off: number): void => { + this.push(formId, kind, off); + }).catch((err: BusinessError): void => { + hilog.warn(DOMAIN, TAG, '卡片翻页失败:%{public}s', err.message); + this.push(formId, kind, cur); + }); }).catch((err: BusinessError): void => { - hilog.warn(DOMAIN, TAG, '卡片翻页失败:%{public}s', err.message); - this.push(formId, cur); + hilog.warn(DOMAIN, TAG, '读卡片偏移失败(按今天处理):%{public}s', err.message); + this.push(formId, kind, 0); }); }).catch((err: BusinessError): void => { - hilog.warn(DOMAIN, TAG, '读卡片偏移失败(按今天处理):%{public}s', err.message); - this.push(formId, 0); + hilog.warn(DOMAIN, TAG, '读卡片类型失败:%{public}s', err.message); }); } diff --git a/entry/src/main/ets/pages/card/FbMovieCard2x2.ets b/entry/src/main/ets/pages/card/FbMovieCard2x2.ets new file mode 100644 index 0000000..d107c6c --- /dev/null +++ b/entry/src/main/ets/pages/card/FbMovieCard2x2.ets @@ -0,0 +1,125 @@ +/** + * 家书 FamilyBook · 2×2 服务卡片(观影 · 今年概览) + * + * 用户 2026-10-24:「在做几个服务卡片,包括观影、任务,你看着做吧。」 + * ⇒ 排版照日程 2×2(FbCard2x2)的骨架:抬头 + 分隔线 + 大数字。 + * 大数字 = **今年**看过的场数(比总数更有"今年看了多少"的当下感); + * 底部一行给最近一部电影,让卡上有具体内容而不只是一个数。 + * + * ⚠️ 动态卡片不能自己发网络请求:只画 `fbMovieCardBuild` 推过来的字段。 + * ⚠️ 整卡可点 → 打开应用并落到「观影」模块(fbModule: 'movies')。 + */ + +let storageMv2 = new LocalStorage(); + +/** 最近观影一行(`recentJson` 的元素,只用到这几个字段) */ +class FbMovieRow2 { + title: string = ''; + dateText: string = ''; +} + +@Entry(storageMv2) +@Component +struct FbMovieCard2x2 { + @LocalStorageProp('state') state: string = 'loading'; + @LocalStorageProp('hint') hint: string = ''; + @LocalStorageProp('updatedAt') updatedAt: string = ''; + @LocalStorageProp('yearCount') yearCount: number = 0; + @LocalStorageProp('recentJson') recentJson: string = '[]'; + /* 🔴 v89 教训:ForEach key 要带 version 前缀(本卡列表最多一条,也照规矩来) */ + @LocalStorageProp('version') version: string = ''; + + /** 最近一部(取不到给空) */ + private latest(): FbMovieRow2 { + try { + const all: FbMovieRow2[] = JSON.parse(this.recentJson) as FbMovieRow2[]; + if (all.length > 0) { + return all[0]; + } + } catch (err) { + // 解析失败按"没有最近一部"处理 + } + const none: FbMovieRow2 = new FbMovieRow2(); + return none; + } + + private openApp(): void { + postCardAction(this, { + action: 'router', + abilityName: 'EntryAbility', + params: { fbModule: 'movies' } + }); + } + + 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) + } + .width('100%') + + Divider() + .strokeWidth(0.5) + .color('#EFE6D8') + + // ---- 今年场数(占满剩余空间,垂直居中)---- + Row({ space: 8 }) { + Text(`${this.yearCount}`) + .fontSize(40) + .fontWeight(FontWeight.Bold) + .fontColor('#7A4E8C') + Column({ space: 2 }) { + Text('今年观影') + .fontSize(12) + .fontColor('#2C231D') + .maxLines(1) + Text(this.hint !== '' ? this.hint : '点击查看') + .fontSize(10) + .fontColor(this.hint !== '' ? '#C0533F' : '#A8988A') + .maxLines(1) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + } + .alignItems(HorizontalAlign.Start) + .layoutWeight(1) + } + .width('100%') + .layoutWeight(1) + .alignItems(VerticalAlign.Center) + + // ---- 最近一部(有数据才画)---- + if (this.latest().title !== '') { + Row({ space: 6 }) { + Column() + .width(3) + .height(26) + .borderRadius(2) + .backgroundColor('#7A4E8C') + Text('《' + this.latest().title + '》') + .fontSize(11) + .fontColor('#5A4A3D') + .maxLines(1) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + .layoutWeight(1) + } + .width('100%') + } + } + .width('100%') + .height('100%') + .padding(14) + .backgroundColor('#FFFFFF') + .borderRadius(16) + // ⭐ 整卡可点 + .onClick((): void => this.openApp()) + } +} diff --git a/entry/src/main/ets/pages/card/FbMovieCard2x4.ets b/entry/src/main/ets/pages/card/FbMovieCard2x4.ets new file mode 100644 index 0000000..5b3aceb --- /dev/null +++ b/entry/src/main/ets/pages/card/FbMovieCard2x4.ets @@ -0,0 +1,195 @@ +/** + * 家书 FamilyBook · 2×4 服务卡片(观影 · 左概览 / 右最近) + * + * 用户 2026-10-24:「在做几个服务卡片,包括观影、任务,你看着做吧。」 + * ⇒ 骨架照日程 2×4(FbCard2x4):左半 = 2×2 那套;右半 = 最近观影(最多三条)。 + * + * ⚠️ 数据与 2×2 同一份(fbMovieCardBuild 算一次),条数口径一致 —— 卡片里不自己算。 + */ + +let storageMv4 = new LocalStorage(); + +/** 右半「最近观影」一行 */ +class FbMovieRow4 { + key: string = ''; + title: string = ''; + dateText: string = ''; + cinema: string = ''; + rating: string = ''; +} + +@Entry(storageMv4) +@Component +struct FbMovieCard2x4 { + @LocalStorageProp('state') state: string = 'loading'; + @LocalStorageProp('hint') hint: string = ''; + @LocalStorageProp('updatedAt') updatedAt: string = ''; + @LocalStorageProp('yearCount') yearCount: number = 0; + @LocalStorageProp('totalCount') totalCount: number = 0; + @LocalStorageProp('recentJson') recentJson: string = '[]'; + /* 🔴 v89 教训:ForEach key 必须带 version 前缀,否则下次推送不重绘 */ + @LocalStorageProp('version') version: string = ''; + + /** 右半最多三条(数据侧已裁到 4,这里再兜底截断) */ + private recent(): FbMovieRow4[] { + try { + const all: FbMovieRow4[] = JSON.parse(this.recentJson) as FbMovieRow4[]; + return all.slice(0, 3); + } catch (err) { + return []; + } + } + + private openApp(): void { + postCardAction(this, { + action: 'router', + abilityName: 'EntryAbility', + params: { fbModule: 'movies' } + }); + } + + /** 右半单条:左色条 + 片名 + 日期/影院(+评分) */ + @Builder + recentRow(item: FbMovieRow4) { + Row({ space: 8 }) { + Column() + .width(3) + .height(38) + .borderRadius(2) + .backgroundColor('#7A4E8C') + Column({ space: 2 }) { + Text(item.title) + .fontSize(13) + .fontColor('#2C231D') + .maxLines(1) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + Row({ space: 4 }) { + if (item.dateText !== '') { + Text(item.dateText) + .fontSize(9) + .fontColor('#A8988A') + .maxLines(1) + } + if (item.cinema !== '') { + Text(item.cinema) + .fontSize(9) + .fontColor('#A8988A') + .maxLines(1) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + .layoutWeight(1) + } + } + .width('100%') + } + .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() { + Row({ space: 10 }) { + // ===== 左半:与 2×2 一致 ===== + 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) + } + .width('100%') + + Divider() + .strokeWidth(0.5) + .color('#EFE6D8') + + Row({ space: 6 }) { + Text(`${this.yearCount}`) + .fontSize(38) + .fontWeight(FontWeight.Bold) + .fontColor('#7A4E8C') + Column({ space: 2 }) { + Text('今年观影') + .fontSize(11) + .fontColor('#2C231D') + .maxLines(1) + Text(this.hint !== '' ? this.hint + : ('共 ' + this.totalCount.toString() + ' 条 · 点击查看')) + .fontSize(9) + .fontColor(this.hint !== '' ? '#C0533F' : '#A8988A') + .maxLines(1) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + } + .alignItems(HorizontalAlign.Start) + } + .width('100%') + .layoutWeight(1) + .alignItems(VerticalAlign.Center) + } + .layoutWeight(1) + .height('100%') + + Divider() + .vertical(true) + .height('92%') + .strokeWidth(0.5) + .color('#EFE6D8') + + // ===== 右半:最近观影(不滚动)===== + Column() { + if (this.recent().length === 0) { + this.emptyHint() + } else { + Column({ space: 6 }) { + ForEach(this.recent(), (item: FbMovieRow4) => { + this.recentRow(item) + }, (item: FbMovieRow4) => `mv_${this.version}_${item.key}`) + } + .width('100%') + .layoutWeight(1) + .justifyContent(FlexAlign.Center) + } + } + .layoutWeight(1) + .height('100%') + } + .width('100%') + .height('100%') + .padding(12) + .backgroundColor('#FFFFFF') + .borderRadius(16) + // ⭐ 整卡可点 + .onClick((): void => this.openApp()) + } +} diff --git a/entry/src/main/ets/pages/card/FbTaskCard2x2.ets b/entry/src/main/ets/pages/card/FbTaskCard2x2.ets new file mode 100644 index 0000000..775301f --- /dev/null +++ b/entry/src/main/ets/pages/card/FbTaskCard2x2.ets @@ -0,0 +1,85 @@ +/** + * 家书 FamilyBook · 2×2 服务卡片(任务 · 待办概览) + * + * 用户 2026-10-24:「在做几个服务卡片,包括观影、任务,你看着做吧。」 + * ⇒ 骨架照日程 2×2;大数字 = 未完成任务数(todo + doing); + * 有逾期时副行变红给「逾期 N 条」,让"该处理了"一眼可见。 + * + * ⚠️ 动态卡片不能自己发网络请求:只画 `fbTaskCardBuild` 推过来的字段。 + * ⚠️ 整卡可点 → 打开应用并落到「任务」模块(fbModule: 'tasks')。 + */ + +let storageTk2 = new LocalStorage(); + +@Entry(storageTk2) +@Component +struct FbTaskCard2x2 { + @LocalStorageProp('state') state: string = 'loading'; + @LocalStorageProp('hint') hint: string = ''; + @LocalStorageProp('updatedAt') updatedAt: string = ''; + @LocalStorageProp('openCount') openCount: number = 0; + @LocalStorageProp('overdueCount') overdueCount: number = 0; + + private openApp(): void { + postCardAction(this, { + action: 'router', + abilityName: 'EntryAbility', + params: { fbModule: 'tasks' } + }); + } + + 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) + } + .width('100%') + + Divider() + .strokeWidth(0.5) + .color('#EFE6D8') + + // ---- 待办数 ---- + Row({ space: 8 }) { + Text(`${this.openCount}`) + .fontSize(40) + .fontWeight(FontWeight.Bold) + .fontColor('#A8574F') + Column({ space: 2 }) { + Text('待办任务') + .fontSize(12) + .fontColor('#2C231D') + .maxLines(1) + 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 }) + } + .alignItems(HorizontalAlign.Start) + .layoutWeight(1) + } + .width('100%') + .layoutWeight(1) + .alignItems(VerticalAlign.Center) + } + .width('100%') + .height('100%') + .padding(14) + .backgroundColor('#FFFFFF') + .borderRadius(16) + // ⭐ 整卡可点 + .onClick((): void => this.openApp()) + } +} diff --git a/entry/src/main/ets/pages/card/FbTaskCard2x4.ets b/entry/src/main/ets/pages/card/FbTaskCard2x4.ets new file mode 100644 index 0000000..86113e9 --- /dev/null +++ b/entry/src/main/ets/pages/card/FbTaskCard2x4.ets @@ -0,0 +1,198 @@ +/** + * 家书 FamilyBook · 2×4 服务卡片(任务 · 左概览 / 右待办) + * + * 用户 2026-10-24:「在做几个服务卡片,包括观影、任务,你看着做吧。」 + * ⇒ 骨架照日程 2×4:左半 = 2×2 那套;右半 = 待办任务(最多三条)。 + * 排序由数据侧定好(逾期 > 进行中 > 普通,同组按截止时间升序), + * 逾期行整条用红色 deadline 文案提示。 + * + * ⚠️ 数据与 2×2 同一份(fbTaskCardBuild 算一次)。 + */ + +let storageTk4 = new LocalStorage(); + +/** 右半「待办」一行 */ +class FbTaskRow4 { + key: string = ''; + title: string = ''; + dueText: string = ''; + status: string = ''; + boardColor: string = '#A8574F'; + overdue: boolean = false; +} + +@Entry(storageTk4) +@Component +struct FbTaskCard2x4 { + @LocalStorageProp('state') state: string = 'loading'; + @LocalStorageProp('hint') hint: string = ''; + @LocalStorageProp('updatedAt') updatedAt: string = ''; + @LocalStorageProp('openCount') openCount: number = 0; + @LocalStorageProp('overdueCount') overdueCount: number = 0; + @LocalStorageProp('rowsJson') rowsJson: string = '[]'; + /* 🔴 v89 教训:ForEach key 必须带 version 前缀,否则下次推送不重绘 */ + @LocalStorageProp('version') version: string = ''; + + /** 右半最多三条(数据侧已裁到 4,这里再兜底截断) */ + private rows(): FbTaskRow4[] { + try { + const all: FbTaskRow4[] = JSON.parse(this.rowsJson) as FbTaskRow4[]; + return all.slice(0, 3); + } catch (err) { + return []; + } + } + + private openApp(): void { + postCardAction(this, { + action: 'router', + abilityName: 'EntryAbility', + params: { fbModule: 'tasks' } + }); + } + + /** 右半单条:左色条 + 标题 + 截止(逾期红) */ + @Builder + taskRow(item: FbTaskRow4) { + Row({ space: 8 }) { + Column() + .width(3) + .height(38) + .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) + } + .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() { + Row({ space: 10 }) { + // ===== 左半:与 2×2 一致 ===== + 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) + } + .width('100%') + + Divider() + .strokeWidth(0.5) + .color('#EFE6D8') + + Row({ space: 6 }) { + Text(`${this.openCount}`) + .fontSize(38) + .fontWeight(FontWeight.Bold) + .fontColor('#A8574F') + Column({ space: 2 }) { + Text('待办任务') + .fontSize(11) + .fontColor('#2C231D') + .maxLines(1) + Text(this.hint !== '' ? this.hint + : (this.overdueCount > 0 ? '逾期 ' + this.overdueCount.toString() + ' 条 · 点击查看' + : '点击查看')) + .fontSize(9) + .fontColor(this.hint !== '' || this.overdueCount > 0 ? '#C0533F' : '#A8988A') + .maxLines(1) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + } + .alignItems(HorizontalAlign.Start) + } + .width('100%') + .layoutWeight(1) + .alignItems(VerticalAlign.Center) + } + .layoutWeight(1) + .height('100%') + + Divider() + .vertical(true) + .height('92%') + .strokeWidth(0.5) + .color('#EFE6D8') + + // ===== 右半:待办(不滚动)===== + Column() { + if (this.rows().length === 0) { + this.emptyHint() + } else { + Column({ space: 6 }) { + ForEach(this.rows(), (item: FbTaskRow4) => { + this.taskRow(item) + }, (item: FbTaskRow4) => `tk_${this.version}_${item.key}`) + } + .width('100%') + .layoutWeight(1) + .justifyContent(FlexAlign.Center) + } + } + .layoutWeight(1) + .height('100%') + } + .width('100%') + .height('100%') + .padding(12) + .backgroundColor('#FFFFFF') + .borderRadius(16) + // ⭐ 整卡可点 + .onClick((): void => this.openApp()) + } +} diff --git a/entry/src/main/resources/base/element/string.json b/entry/src/main/resources/base/element/string.json index d5bd9e4..d42820a 100644 --- a/entry/src/main/resources/base/element/string.json +++ b/entry/src/main/resources/base/element/string.json @@ -35,6 +35,22 @@ { "name": "card_6x4_name", "value": "家书 · 日程(宽)" + }, + { + "name": "card_movie_2x2_name", + "value": "家书 · 观影" + }, + { + "name": "card_movie_2x4_name", + "value": "家书 · 最近观影" + }, + { + "name": "card_task_2x2_name", + "value": "家书 · 待办" + }, + { + "name": "card_task_2x4_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 4fe1873..0ac2bfa 100644 --- a/entry/src/main/resources/base/profile/form_config.json +++ b/entry/src/main/resources/base/profile/form_config.json @@ -67,6 +67,74 @@ "updateDuration": 1, "defaultDimension": "6*4", "supportDimensions": ["6*4"] + }, + { + "name": "FbMovieCard2x2", + "displayName": "$string:card_movie_2x2_name", + "description": "$string:card_desc", + "src": "./ets/pages/card/FbMovieCard2x2.ets", + "window": { + "designWidth": 720, + "autoDesignWidth": true + }, + "colorMode": "light", + "uiSyntax": "arkts", + "isDefault": false, + "updateEnabled": true, + "updateDuration": 1, + "defaultDimension": "2*2", + "supportDimensions": ["2*2"] + }, + { + "name": "FbMovieCard2x4", + "displayName": "$string:card_movie_2x4_name", + "description": "$string:card_desc", + "src": "./ets/pages/card/FbMovieCard2x4.ets", + "window": { + "designWidth": 720, + "autoDesignWidth": true + }, + "colorMode": "light", + "uiSyntax": "arkts", + "isDefault": false, + "updateEnabled": true, + "updateDuration": 1, + "defaultDimension": "2*4", + "supportDimensions": ["2*4"] + }, + { + "name": "FbTaskCard2x2", + "displayName": "$string:card_task_2x2_name", + "description": "$string:card_desc", + "src": "./ets/pages/card/FbTaskCard2x2.ets", + "window": { + "designWidth": 720, + "autoDesignWidth": true + }, + "colorMode": "light", + "uiSyntax": "arkts", + "isDefault": false, + "updateEnabled": true, + "updateDuration": 1, + "defaultDimension": "2*2", + "supportDimensions": ["2*2"] + }, + { + "name": "FbTaskCard2x4", + "displayName": "$string:card_task_2x4_name", + "description": "$string:card_desc", + "src": "./ets/pages/card/FbTaskCard2x4.ets", + "window": { + "designWidth": 720, + "autoDesignWidth": true + }, + "colorMode": "light", + "uiSyntax": "arkts", + "isDefault": false, + "updateEnabled": true, + "updateDuration": 1, + "defaultDimension": "2*4", + "supportDimensions": ["2*4"] } ] }