v69:日程桌面卡片完全照 SyncCalendar 重做(四种尺寸 + 翻页 + 时间轴)
用户 2026-10-20:「跟日程有关的桌面服务卡片,你就完全按照 synccalendar 来做就行。」
上一版卡片是"首页概览"(今日日程 + 在做/待办/逾期 + 纪念日),
是照着首页数据自己设计的版式。这一批按用户要求换成 **SyncCalendar 那套日程卡**:
## 四块尺寸(与 SyncCalendar 一一对应)
- 2×2 日期 / 星期+农历 / 当天条数大数字 + +
- 2×4 左半同 2×2,右半「接下来两条」
- 4×4 `‹ 10月20日 / 九月初一 › [今|+]` + 当天日程(时间轴 / 列表)
- 6×4 同 4×4,更宽(小时 40vp)
## 翻页(这是"照 SyncCalendar"的核心)
- 卡片按钮走 `postCardAction({action:'message', params:{pageAction}})`,
`FbFormAbility.onFormEvent` 改偏移 → 重新取数 → 推回卡片;
- **每张卡一份偏移量**,落 `preferences`(`data/FbCardForms.ets`):
SyncCalendar 只放内存 Map,进程被回收就丢 ⇒ 系统每 30 分钟刷新一次,
会把用户翻到的"下周三"悄悄拽回今天。家书这边连 `updateDuration` 的定时刷新
也按各自偏移重建(`onUpdateForm` 先 `getOffset` 再 `push`)。
- 偏移夹取 ±366 天:message 是外部输入,算到离谱年份会 Invalid Date(白卡)。
## 点卡片要落到"卡片显示的那一天"
- 卡片 router 带 `openDate`(**毫秒字符串**,数字参数是 int32,13 位会溢出)
与 `widgetAction=add`(点的是+);
- `EntryAbility.applyWantModule` 解析后写 `FbAppState.calJumpIso/Create/Seq`;
- 日程页两条入口都接上:冷启动 `aboutToAppear`、热启动 `@Monitor('app.calJumpSeq')`
(应用开着时点卡片不会重建页面,只靠 aboutToAppear 会"点了没反应")。
## 数据
- 换接口:`/api/v1/calendar?from=<那天>&to=<那天>`(原来打 `/home`)。
`authProbe: true` + 显式带 token 的硬要求不变(后台 401 不能把用户踢下线)。
- 农历行(`kind=lunar`)抽成抬头那一行小字,其余衍生条目(节气/节假日)照常进列表。
- 时间轴复用 `common/FbTimeline`(与日程页同一套算法),
卡片侧只做"比例 → vp"的换算(`fbCardTlLines`,纯函数、可断言)。
- 显示方式(时间轴/列表)跟「日程设置」联动;今天才画当前时间红线。
- 应用内改完日程 → `FB_CARD_FORMS.pushAll()` 立刻刷新桌面卡片
(不必等 30 分钟;推失败自动注销该 formId)。
## 验证
- `assembleHap` BUILD SUCCESSFUL;已装机(5NC0225B27004718)。
- `_fb_arkts_guard.js` **856 / 0**(v66 组按新契约重写:四尺寸登记、
字段名与数据类配对、数组必须 JSON 字符串、翻页三动作、
`onUpdateForm` 按偏移重建、冷/热启动两条入口)。
- `_fb_v59d_falsify.py` **146 / 0**(新增 6 条注入:定时刷新丢偏移、
偏移不落盘、4×4 少「下一天」、「热启动不响应」、不读 openDate、
字段名拼错、去 authProbe)。
⚠️ 其中"热启动"那条第一次注入**打偏了**:它先命中了文档注释里那半句
`@Monitor('app.calJumpSeq')` ⇒ 真正的装饰器没被改,守卫照样绿。
已把字面量从注释里去掉(本项目"注释里别抄字面标签"的老坑)。
- 真机跳转 `_fb_v69_card_jump.py`:
· `openDate=今天+3天` + `widgetAction=add` ⇒ 日程页落在 **10月23日**,
且「新建日程」浮层打开、日期就是 10月23日(dump 里 26 个浮层节点);
· 只带 `openDate` ⇒ 同样落在 10月23日,**不**弹新建浮层。
This commit is contained in:
@@ -36,6 +36,25 @@ export class FbAppState {
|
|||||||
/** 当前所在模块 key(home / people / movies …) */
|
/** 当前所在模块 key(home / people / movies …) */
|
||||||
@Trace activeModule: string = 'home';
|
@Trace activeModule: string = 'home';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 桌面日程卡片点进来时要落到哪一天(`YYYY-MM-DD`,空 = 没有卡片跳转请求)。
|
||||||
|
*
|
||||||
|
* 🔴 用户 2026-10-20:「跟日程有关的桌面服务卡片,你就完全按照 synccalendar 来做就行。」
|
||||||
|
* SyncCalendar 的做法是卡片 `router` 时带 `openDate`(毫秒时间戳字符串),
|
||||||
|
* 应用启动后把日程页定位到那一天。
|
||||||
|
*
|
||||||
|
* ⚠️ 为什么要做成**三个 @Trace 字段**而不是一个"待处理参数"对象:
|
||||||
|
* ① 卡片可以在应用**已经开着**时被点(热启动走 `onNewWant`,页面不会重建)
|
||||||
|
* ⇒ 必须有一个会触发重渲染的信号,所以用 `calJumpSeq` 自增;
|
||||||
|
* ② 页面是 `@ComponentV2`,`@Monitor` 只能盯**可观察的字段路径**
|
||||||
|
* ⇒ 放在 `FbAppState`(`@ObservedV2`)里最省事,且 Index/日程页都能看到。
|
||||||
|
*/
|
||||||
|
@Trace calJumpIso: string = '';
|
||||||
|
/** 卡片上是「+」进来的 ⇒ 进日程页后直接开"新建日程"浮层 */
|
||||||
|
@Trace calJumpCreate: boolean = false;
|
||||||
|
/** 跳转请求序号(页面 `@Monitor` 它;同一个日期连点两次也能再触发一次) */
|
||||||
|
@Trace calJumpSeq: number = 0;
|
||||||
|
|
||||||
/** 是否宽屏(窗口短边 ≥ 600vp)—— 决定抽屉是「常驻侧栏」还是「滑出层」 */
|
/** 是否宽屏(窗口短边 ≥ 600vp)—— 决定抽屉是「常驻侧栏」还是「滑出层」 */
|
||||||
@Trace wideLayout: boolean = false;
|
@Trace wideLayout: boolean = false;
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,23 @@
|
|||||||
/**
|
/**
|
||||||
* 家书 FamilyBook · 桌面服务卡片数据(鸿蒙端)
|
* 家书 FamilyBook · **日程**桌面卡片数据(鸿蒙端)
|
||||||
*
|
*
|
||||||
* 用户 2026-10-20:「做几个和 syncCalendar 差不多的卡片。」
|
* 用户 2026-10-20:「跟日程有关的桌面服务卡片,你就完全按照 synccalendar 来做就行。」
|
||||||
* ⇒ 参照 `SyncCalendar` 的 `CardDataService`:卡片**不直连数据库**,
|
* ⇒ 结构照 `D:\Yongquan\code\SyncCalendar` 的 `CardDataService` + 四块卡:
|
||||||
* 而是把"卡片要显示的那几十个字段"先算成一份**扁平的**数据对象
|
* · 2×2 日期 / 星期农历 / 当天条数;
|
||||||
* (每个字段都是 string / number),再由 `formBindingData` 推给卡片渲染。
|
* · 2×4 左边同 2×2,右边「今天接下来两条」;
|
||||||
|
* · 4×4 日期翻页条 + 当天日程(时间轴 / 列表,跟应用内设置联动);
|
||||||
|
* · 6×4 同 4×4,横向更宽。
|
||||||
*
|
*
|
||||||
* 为什么不让卡片自己去请求接口:
|
* 与 SyncCalendar 的**两处有意差异**(都是家书的数据形状决定的,不照抄):
|
||||||
* ① 卡片的 ArkTS 是**受限子集**(不能随便 await、不能拿 Context 去读 preferences),
|
* ① 它读端上本地库,家书读服务端 `GET /api/v1/calendar?from=&to=`;
|
||||||
* 它只认 `LocalStorage` 里那几个标量;
|
* ② 它的数据里没有"衍生条目",家书的 `/calendar` 会给农历 / 节气 / 节假日
|
||||||
* ② 逻辑放在这里 ⇒ 2×2 / 2×4 / 4×4 三种尺寸共用**同一份计算**,
|
* ⇒ 农历行抽成 `lunarText`(抬头那一行),其余衍生条目照常进列表。
|
||||||
* 不会出现"大卡说 5 条日程、小卡说 3 条"这种自相矛盾。
|
|
||||||
*
|
*
|
||||||
* 🔴 数据来源只用 **`GET /api/v1/home` 一个接口**:
|
* 🔴 三条硬约束(沿用上一版卡片,别丢):
|
||||||
* 它一次给齐 日期/星期/农历/日子标记/今日日程/统计/最近纪念日,
|
* ① 卡片进程生命周期回调**只活约 10 秒** ⇒ 回调里不同步做长任务;
|
||||||
* 与网页版首页**同源**(服务端 `home()` 的注释写明了这一点)。
|
* ② `FormBindingData` **只认标量** ⇒ 数组一律 `JSON.stringify` 成字符串;
|
||||||
* 多打两个接口(calendar + days)只会让卡片刷新变慢、且口径可能与首页不一致。
|
* ③ 卡片是**后台**刷新,请求必须 `authProbe: true` 且显式带 token ——
|
||||||
*
|
* 否则一次 401 会触发全局"踢回登录页",用户在应用里用得好好的却被卡片踢下线。
|
||||||
* ⚠️ 令牌与服务器地址:
|
|
||||||
* 卡片进程(FormExtensionAbility)与主应用**共享同一个沙箱**,
|
|
||||||
* 所以直接复用 `FbTokenStore` / `FbServerStore` 的 preferences 即可,
|
|
||||||
* **不要在卡片里另存一份**(那样用户在应用里"退出登录",卡片还带着老令牌)。
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { common } from '@kit.AbilityKit';
|
import { common } from '@kit.AbilityKit';
|
||||||
@@ -29,68 +26,121 @@ import { hilog } from '@kit.PerformanceAnalysisKit';
|
|||||||
import { BusinessError } from '@kit.BasicServicesKit';
|
import { BusinessError } from '@kit.BasicServicesKit';
|
||||||
import { FB_SERVER_STORE } from './FbServerStore';
|
import { FB_SERVER_STORE } from './FbServerStore';
|
||||||
import { FB_TOKEN_STORE } from './FbTokenStore';
|
import { FB_TOKEN_STORE } from './FbTokenStore';
|
||||||
|
import { FB_CAL_SETTINGS } from './FbCalSettings';
|
||||||
import { fbApi, FbApiResult } from './FbHttp';
|
import { fbApi, FbApiResult } from './FbHttp';
|
||||||
import { parseHome } from './FbParse';
|
import { parseCalendar } from './FbParse';
|
||||||
import { FbCalEvent, FbHomeData, FbMark, FbRecentDay } from '../model/Types';
|
import { FbCalEvent } from '../model/Types';
|
||||||
|
import { FbTimelineUtil, FbTimelineData, FbTimeBlock, FbViewRange } from '../common/FbTimeline';
|
||||||
|
|
||||||
const DOMAIN: number = 0x0000;
|
const DOMAIN: number = 0x0000;
|
||||||
const TAG: string = 'FbCard';
|
const TAG: string = 'FbCard';
|
||||||
|
|
||||||
/** 卡片上的一条日程(扁平化:卡片只认标量) */
|
/** 卡片上的一行日程(扁平标量,卡片侧 `JSON.parse` 后 ForEach) */
|
||||||
export class FbCardEvent {
|
export class FbCardRow {
|
||||||
/** ForEach 的 key(**必须唯一**,否则同名的两条只渲染第一条) */
|
/** ForEach 的 key(**必须唯一**:同名两条只渲染一条,踩过) */
|
||||||
key: string = '';
|
key: string = '';
|
||||||
/** `09:00` / `全天` */
|
/** `09:00` / `全天` */
|
||||||
time: string = '';
|
time: string = '';
|
||||||
|
/** `10:30`(全天为空) */
|
||||||
|
endTime: string = '';
|
||||||
title: string = '';
|
title: string = '';
|
||||||
/** 日历本色标(直接当左侧色条用) */
|
/** 所属日历本色标(左侧色条) */
|
||||||
color: string = '#A84A28';
|
color: string = '#A84A28';
|
||||||
|
/** 所属日历本名(4×4 列表右侧小字) */
|
||||||
|
calName: string = '';
|
||||||
|
/** 正在进行中(仅"今天"的有时间日程) */
|
||||||
|
isNow: boolean = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 卡片上的一条纪念日倒计时 */
|
/** 时间轴上的一块(比例形式;视图侧按小时高度换算成 vp) */
|
||||||
export class FbCardDay {
|
export class FbCardBlock {
|
||||||
key: string = '';
|
key: string = '';
|
||||||
title: string = '';
|
title: string = '';
|
||||||
/** 还有几天(0 = 就是今天) */
|
timeText: string = '';
|
||||||
days: number = 0;
|
color: string = '#A84A28';
|
||||||
/** 7 天内 ⇒ 卡片上用品牌色强调 */
|
/** 距 0 点的比例 0~1 */
|
||||||
soon: boolean = false;
|
topRatio: number = 0;
|
||||||
|
heightRatio: number = 0;
|
||||||
|
/** 冲突组序号(同一簇重叠日程) */
|
||||||
|
groupIndex: number = 0;
|
||||||
|
/** 组内列号(同列互不重叠) */
|
||||||
|
laneIndex: number = 0;
|
||||||
|
isNow: boolean = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 时间轴渲染的一"行":要么是一段空白,要么是一个冲突组(组内按列并排) */
|
||||||
|
export class FbCardTlBlock {
|
||||||
|
key: string = '';
|
||||||
|
title: string = '';
|
||||||
|
timeText: string = '';
|
||||||
|
color: string = '#A84A28';
|
||||||
|
isNow: boolean = false;
|
||||||
|
/** 组内上方的空白高度(vp) */
|
||||||
|
padTopVp: number = 0;
|
||||||
|
/** 色块高度(vp) */
|
||||||
|
heightVp: number = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FbCardTlGroup {
|
||||||
|
key: string = '';
|
||||||
|
heightVp: number = 0;
|
||||||
|
lanes: FbCardTlBlock[][] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FbCardTlLine {
|
||||||
|
key: string = '';
|
||||||
|
/** 空白行高度(kind=0 时有效) */
|
||||||
|
blankVp: number = 0;
|
||||||
|
/** kind=1 时有效 */
|
||||||
|
group: FbCardTlGroup | null = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 卡片整体数据。
|
* 卡片整体数据。
|
||||||
*
|
*
|
||||||
* ⚠️ **字段名就是 `LocalStorage` 的键名**(卡片页 `@LocalStorageProp('xxx')`),
|
* ⚠️ **字段名就是 `LocalStorage` 的键名**(卡片页 `@LocalStorageProp('xxx')`),
|
||||||
* 两侧改名必须同步 —— 改一边的表现是"卡片永远显示默认值",不会报错。
|
* 两侧改名必须同步 —— 改一边的表现是"卡片永远显示默认值",编译不报错。
|
||||||
*/
|
*/
|
||||||
export class FbCardData {
|
export class FbCardData {
|
||||||
/**
|
/** `ok` / `login` / `noserver` / `offline` / `loading` */
|
||||||
* 状态:`ok` 正常 / `login` 未登录 / `noserver` 没填服务器 /
|
|
||||||
* `offline` 连不上(此时若有缓存则显示缓存) / `loading` 刚添加还没拉到。
|
|
||||||
*/
|
|
||||||
state: string = 'loading';
|
state: string = 'loading';
|
||||||
/** `10 月 20 日` */
|
|
||||||
todayLabel: string = '';
|
|
||||||
/** `星期二` */
|
|
||||||
weekday: string = '';
|
|
||||||
/** `九月初一`(空 = 算不出,卡片不画) */
|
|
||||||
lunarShort: string = '';
|
|
||||||
/** 日子标记拼成一行:`寒露 · 重阳节` */
|
|
||||||
marks: string = '';
|
|
||||||
/** 今日日程条数 */
|
|
||||||
eventCount: number = 0;
|
|
||||||
/** 在做 / 待办 / 逾期(三个数字与首页概述卡同源) */
|
|
||||||
doing: number = 0;
|
|
||||||
todo: number = 0;
|
|
||||||
overdue: number = 0;
|
|
||||||
/** `FbCardEvent[]` 的 JSON(卡片侧 `JSON.parse` 成数组再 ForEach) */
|
|
||||||
eventsJson: string = '[]';
|
|
||||||
/** `FbCardDay[]` 的 JSON */
|
|
||||||
daysJson: string = '[]';
|
|
||||||
/** 一行小字提示(未登录 / 离线 / 出错时给用户一个说法) */
|
/** 一行小字提示(未登录 / 离线 / 出错时给用户一个说法) */
|
||||||
hint: string = '';
|
hint: string = '';
|
||||||
/** `15:40 更新` */
|
/** `15:40 更新` */
|
||||||
updatedAt: string = '';
|
updatedAt: string = '';
|
||||||
|
/** `10月20日` */
|
||||||
|
dateMd: string = '';
|
||||||
|
/** `今天 10月20日 周二` */
|
||||||
|
dateText: string = '';
|
||||||
|
/** `星期二` */
|
||||||
|
weekday: string = '';
|
||||||
|
/** `九月初一`(空 = 算不出 / 那天没有农历行,卡片不画) */
|
||||||
|
lunarText: string = '';
|
||||||
|
/** 相对今天的天数偏移(0 = 今天) */
|
||||||
|
dayOffset: number = 0;
|
||||||
|
/** 当前显示的是不是今天(只有今天画"当前时间"红线) */
|
||||||
|
isToday: boolean = true;
|
||||||
|
/** 当前显示那天 0 点的毫秒(点卡片进应用时定位到这一天) */
|
||||||
|
selectedDate: number = 0;
|
||||||
|
/** 当天条目数(含全天) */
|
||||||
|
dayCount: number = 0;
|
||||||
|
/** 当天日程(行式,全天在前、其余按时间升序) */
|
||||||
|
listJson: string = '[]';
|
||||||
|
/** 全天/跨天条目(单独一组,画在时间轴上方) */
|
||||||
|
allDayJson: string = '[]';
|
||||||
|
/** 时间轴色块(`FbCardBlock[]`) */
|
||||||
|
timelineJson: string = '[]';
|
||||||
|
/** 「接下来两条」(2×4 右半用) */
|
||||||
|
upcomingJson: string = '[]';
|
||||||
|
/** 日程画法:`timeline` / `list`(与应用内「日程设置」联动) */
|
||||||
|
displayStyle: string = 'timeline';
|
||||||
|
/** 时间轴视窗(小时,含起点不含终点) */
|
||||||
|
viewStartHour: number = 8;
|
||||||
|
viewEndHour: number = 18;
|
||||||
|
/** 当前时刻在当天时间轴上的比例;**-1 = 不画红线**(非今天) */
|
||||||
|
nowRatio: number = -1;
|
||||||
|
/** `15:40` */
|
||||||
|
nowLabel: string = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 两位补零 */
|
/** 两位补零 */
|
||||||
@@ -98,74 +148,31 @@ function pad2(v: number): string {
|
|||||||
return v < 10 ? '0' + v.toString() : v.toString();
|
return v < 10 ? '0' + v.toString() : v.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 当前时刻 `HH:mm`(卡片右上角"更新时间") */
|
/** 当前时刻 `HH:mm` */
|
||||||
function clockText(): string {
|
function clockText(): string {
|
||||||
const d: Date = new Date();
|
const d: Date = new Date();
|
||||||
return pad2(d.getHours()) + ':' + pad2(d.getMinutes());
|
return pad2(d.getHours()) + ':' + pad2(d.getMinutes());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 把接口载荷铺进卡片数据 */
|
const FULL_WEEK: string[] = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
|
||||||
function applyHome(home: FbHomeData, out: FbCardData): void {
|
const SHORT_WEEK: string[] = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
|
||||||
out.todayLabel = home.todayLabel;
|
|
||||||
out.weekday = home.weekday;
|
/** `Date` → `YYYY-MM-DD`(**本地时区**:服务端也按本地自然日给条目) */
|
||||||
out.lunarShort = home.lunarShort;
|
function isoOf(d: Date): string {
|
||||||
out.eventCount = home.todayEvents.length;
|
return d.getFullYear().toString() + '-' + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate());
|
||||||
out.doing = home.stat.doing;
|
|
||||||
out.todo = home.stat.todo;
|
|
||||||
out.overdue = home.stat.overdue;
|
|
||||||
// 日子标记:最多两个(卡片上那一行放三个就会折行,反而看不清)
|
|
||||||
let marks: string = '';
|
|
||||||
let n: number = 0;
|
|
||||||
for (const mk of home.marks as FbMark[]) {
|
|
||||||
if (n >= 2) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
marks += (marks === '' ? '' : ' · ') + mk.word;
|
|
||||||
n++;
|
|
||||||
}
|
|
||||||
out.marks = marks;
|
|
||||||
// 今日日程:最多 6 条(4×4 也只放得下 5~6 行)
|
|
||||||
const events: FbCardEvent[] = [];
|
|
||||||
let idx: number = 0;
|
|
||||||
for (const e of home.todayEvents as FbCalEvent[]) {
|
|
||||||
if (idx >= 6) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
const it: FbCardEvent = new FbCardEvent();
|
|
||||||
it.key = 'e' + idx.toString();
|
|
||||||
it.time = e.allDay || e.time === '' ? '全天' : e.time;
|
|
||||||
it.title = e.title === '' ? '(无标题)' : e.title;
|
|
||||||
it.color = e.color !== '' ? e.color : '#A84A28';
|
|
||||||
events.push(it);
|
|
||||||
idx++;
|
|
||||||
}
|
|
||||||
out.eventsJson = JSON.stringify(events);
|
|
||||||
// 最近纪念日:最多 3 条(服务端已按"最近的在前"排好,**不要再排**)
|
|
||||||
const days: FbCardDay[] = [];
|
|
||||||
let di: number = 0;
|
|
||||||
for (const rd of home.recentDays as FbRecentDay[]) {
|
|
||||||
if (di >= 3) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
const it: FbCardDay = new FbCardDay();
|
|
||||||
it.key = 'd' + di.toString();
|
|
||||||
it.title = rd.title;
|
|
||||||
it.days = rd.days;
|
|
||||||
it.soon = rd.days <= 7;
|
|
||||||
days.push(it);
|
|
||||||
di++;
|
|
||||||
}
|
|
||||||
out.daysJson = JSON.stringify(days);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 缓存用的 preferences(卡片断网时显示上次成功的数据) */
|
/* ══════════════════════════════════════════════════════════════
|
||||||
const CACHE_STORE: string = 'fb_card';
|
缓存(断网时显示上次成功取到的数据)
|
||||||
const CACHE_KEY: string = 'home_json';
|
══════════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
async function saveCache(ctx: common.Context, payload: string): Promise<void> {
|
const CACHE_STORE: string = 'fb_card';
|
||||||
|
const CACHE_PREFIX: string = 'd_';
|
||||||
|
|
||||||
|
async function saveCache(ctx: common.Context, iso: string, payload: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const store: preferences.Preferences = await preferences.getPreferences(ctx, CACHE_STORE);
|
const store: preferences.Preferences = await preferences.getPreferences(ctx, CACHE_STORE);
|
||||||
await store.put(CACHE_KEY, payload);
|
await store.put(CACHE_PREFIX + iso, payload);
|
||||||
// ⚠️ 必须 flush:卡片进程随时可能被回收,不 flush 等于没写
|
// ⚠️ 必须 flush:卡片进程随时可能被回收,不 flush 等于没写
|
||||||
await store.flush();
|
await store.flush();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -174,28 +181,168 @@ async function saveCache(ctx: common.Context, payload: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadCache(ctx: common.Context): Promise<string> {
|
async function loadCache(ctx: common.Context, iso: string): Promise<string> {
|
||||||
try {
|
try {
|
||||||
const store: preferences.Preferences = await preferences.getPreferences(ctx, CACHE_STORE);
|
const store: preferences.Preferences = await preferences.getPreferences(ctx, CACHE_STORE);
|
||||||
const v: preferences.ValueType = await store.get(CACHE_KEY, '');
|
const v: preferences.ValueType = await store.get(CACHE_PREFIX + iso, '');
|
||||||
return typeof v === 'string' ? v : '';
|
return typeof v === 'string' ? v : '';
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ══════════════════════════════════════════════════════════════
|
||||||
|
组装
|
||||||
|
══════════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
/** 这一天第几分钟(`HH:mm` → 分钟;认不出返回 -1) */
|
||||||
|
function minutesOf(hhmm: string): number {
|
||||||
|
return FbTimelineUtil.minutesOf(hhmm);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 一条日程 → 卡片行 */
|
||||||
|
function rowOf(e: FbCalEvent, idx: number, nowMin: number, isToday: boolean): FbCardRow {
|
||||||
|
const r: FbCardRow = new FbCardRow();
|
||||||
|
r.key = 'r' + idx.toString() + '_' + e.kind + '_' + e.id.toString() + '_' + e.time;
|
||||||
|
const allDay: boolean = e.allDay || minutesOf(e.time) < 0;
|
||||||
|
r.time = allDay ? '全天' : e.time;
|
||||||
|
r.endTime = allDay ? '' : e.endTime;
|
||||||
|
// 标题为空时用日历本名兜底(农历/节气这类衍生条目),别显示"(无标题)"
|
||||||
|
r.title = e.title !== '' ? e.title : (e.calName !== '' ? e.calName : '未命名日程');
|
||||||
|
r.color = e.color;
|
||||||
|
r.calName = e.calName;
|
||||||
|
if (!allDay && isToday) {
|
||||||
|
const s: number = minutesOf(e.time);
|
||||||
|
let en: number = minutesOf(e.endTime);
|
||||||
|
if (en <= s) {
|
||||||
|
en = s + 60;
|
||||||
|
}
|
||||||
|
r.isNow = nowMin >= s && nowMin < en;
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 全天在前、其余按开始时间升序(时间相同按标题,保证顺序稳定) */
|
||||||
|
function byDayOrder(a: FbCalEvent, b: FbCalEvent): number {
|
||||||
|
const sa: number = a.allDay || minutesOf(a.time) < 0 ? -1 : minutesOf(a.time);
|
||||||
|
const sb: number = b.allDay || minutesOf(b.time) < 0 ? -1 : minutesOf(b.time);
|
||||||
|
if (sa !== sb) {
|
||||||
|
return sa - sb;
|
||||||
|
}
|
||||||
|
return a.title.localeCompare(b.title);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 把接口行铺进卡片数据 */
|
||||||
|
function applyRows(rows: FbCalEvent[], out: FbCardData, now: Date): void {
|
||||||
|
const nowMin: number = now.getHours() * 60 + now.getMinutes();
|
||||||
|
|
||||||
|
// 1) 农历行 → 抬头那行小字(它的信息量就在"九月初一"这几个字,不必再占一行列表)
|
||||||
|
const events: FbCalEvent[] = [];
|
||||||
|
for (const e of rows) {
|
||||||
|
if (e.kind === 'lunar') {
|
||||||
|
if (out.lunarText === '') {
|
||||||
|
out.lunarText = e.title;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
events.push(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
events.sort(byDayOrder);
|
||||||
|
|
||||||
|
// 2) 行式列表(4×4 列表模式 / 6×4)
|
||||||
|
const list: FbCardRow[] = [];
|
||||||
|
const allDay: FbCardRow[] = [];
|
||||||
|
let idx: number = 0;
|
||||||
|
for (const e of events) {
|
||||||
|
const r: FbCardRow = rowOf(e, idx, nowMin, out.isToday);
|
||||||
|
list.push(r);
|
||||||
|
if (r.time === '全天') {
|
||||||
|
allDay.push(r);
|
||||||
|
}
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
out.listJson = JSON.stringify(list);
|
||||||
|
out.allDayJson = JSON.stringify(allDay);
|
||||||
|
out.dayCount = list.length;
|
||||||
|
|
||||||
|
// 3) 时间轴(只放"带时间"的条目;全天另画在顶部 —— 与 `FbTimelineUtil` 的约定一致)
|
||||||
|
const tl: FbTimelineData = FbTimelineUtil.build(events, out.isToday, nowMin);
|
||||||
|
const blocks: FbCardBlock[] = [];
|
||||||
|
for (const b of tl.blocks as FbTimeBlock[]) {
|
||||||
|
const cb: FbCardBlock = new FbCardBlock();
|
||||||
|
cb.key = b.key;
|
||||||
|
cb.title = b.title;
|
||||||
|
cb.timeText = b.timeText;
|
||||||
|
cb.color = b.color;
|
||||||
|
cb.topRatio = b.topRatio;
|
||||||
|
cb.heightRatio = b.heightRatio;
|
||||||
|
cb.groupIndex = b.groupIndex;
|
||||||
|
cb.laneIndex = b.laneIndex;
|
||||||
|
cb.isNow = b.isNow;
|
||||||
|
blocks.push(cb);
|
||||||
|
}
|
||||||
|
out.timelineJson = JSON.stringify(blocks);
|
||||||
|
const range: FbViewRange = FbTimelineUtil.viewRange(tl.blocks);
|
||||||
|
out.viewStartHour = range.startHour;
|
||||||
|
out.viewEndHour = range.endHour;
|
||||||
|
|
||||||
|
// 4) 「接下来两条」(2×4 右半):今天取"还没开始的",别的日期取当天头两条
|
||||||
|
const upcoming: FbCardRow[] = [];
|
||||||
|
for (const r of list) {
|
||||||
|
if (upcoming.length >= 2) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (r.time === '全天') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (out.isToday && minutesOf(r.time) < nowMin) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
upcoming.push(r);
|
||||||
|
}
|
||||||
|
// 今天已经没有"还没开始的"了 ⇒ 退回头两条有时间的(别让右半空着说"今日日程已结束")
|
||||||
|
if (upcoming.length === 0) {
|
||||||
|
for (const r of list) {
|
||||||
|
if (upcoming.length >= 2) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (r.time !== '全天') {
|
||||||
|
upcoming.push(r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.upcomingJson = JSON.stringify(upcoming);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 组装一份卡片数据(异步)。
|
* 组装一份卡片数据(异步)。
|
||||||
*
|
*
|
||||||
|
* @param dayOffset 相对今天的偏移(卡片翻页:负=过去,0=今天,正=未来)
|
||||||
|
*
|
||||||
* ⚠️ **永远不抛错**:卡片进程抛出去只会让桌面显示一张空白卡。
|
* ⚠️ **永远不抛错**:卡片进程抛出去只会让桌面显示一张空白卡。
|
||||||
* 所有失败都翻译成 `state` + `hint`,由卡片自己画一句人话。
|
|
||||||
*/
|
*/
|
||||||
export async function fbCardBuild(ctx: common.Context): Promise<FbCardData> {
|
export async function fbCardBuild(ctx: common.Context, dayOffset: number): Promise<FbCardData> {
|
||||||
const out: FbCardData = new FbCardData();
|
const out: FbCardData = new FbCardData();
|
||||||
|
const now: Date = new Date();
|
||||||
out.updatedAt = clockText();
|
out.updatedAt = clockText();
|
||||||
|
out.nowLabel = out.updatedAt;
|
||||||
|
out.dayOffset = dayOffset;
|
||||||
|
out.isToday = dayOffset === 0;
|
||||||
|
out.nowRatio = out.isToday ? (now.getHours() * 60 + now.getMinutes()) / 1440 : -1;
|
||||||
|
|
||||||
|
const base: Date = new Date(now.getFullYear(), now.getMonth(), now.getDate() + dayOffset);
|
||||||
|
out.selectedDate = base.getTime();
|
||||||
|
out.dateMd = (base.getMonth() + 1).toString() + '月' + base.getDate().toString() + '日';
|
||||||
|
out.weekday = FULL_WEEK[base.getDay()];
|
||||||
|
const prefix: string = dayOffset === 0 ? '今天 ' : (dayOffset === 1 ? '明天 ' : (dayOffset === -1 ? '昨天 ' : ''));
|
||||||
|
out.dateText = prefix + out.dateMd + ' ' + SHORT_WEEK[base.getDay()];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await FB_SERVER_STORE.init(ctx);
|
await FB_SERVER_STORE.init(ctx);
|
||||||
await FB_TOKEN_STORE.init(ctx);
|
await FB_TOKEN_STORE.init(ctx);
|
||||||
|
// 显示方式(时间轴 / 列表)与应用内设置联动 —— 与 SyncCalendar 的 displayStyle 同一个思路
|
||||||
|
await FB_CAL_SETTINGS.init(ctx);
|
||||||
|
out.displayStyle = FB_CAL_SETTINGS.getStyle();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
hilog.warn(DOMAIN, TAG, '卡片环境预加载失败:%{public}s', JSON.stringify(err));
|
hilog.warn(DOMAIN, TAG, '卡片环境预加载失败:%{public}s', JSON.stringify(err));
|
||||||
}
|
}
|
||||||
@@ -210,21 +357,31 @@ export async function fbCardBuild(ctx: common.Context): Promise<FbCardData> {
|
|||||||
out.hint = '先在「家书」里登录';
|
out.hint = '先在「家书」里登录';
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
/* 🔴 `authProbe: true`:卡片是**后台**刷新,401 时**绝不能**触发全局"踢回登录页"
|
|
||||||
(那个回调会把令牌清掉 —— 用户在应用里用得好好的,桌面卡片一刷新把他踢下线,
|
const iso: string = isoOf(base);
|
||||||
这是最说不清的 bug)。这里自己处理"登录失效"的提示即可。
|
/* 只查**这一天**(服务端的 `from`/`to` 都是闭区间)。
|
||||||
⚠️ 代价:`authProbe` 会跳过 `fbApi` 的"自动补令牌",所以**必须显式传 token**。 */
|
⚠️ 查询参数要显式声明成 `Record<string, string>` 再赋值:
|
||||||
const r: FbApiResult = await fbApi('/api/v1/home', { token: token, authProbe: true });
|
ArkTS 不允许"字面量直接塞进 `Record`",会报 arkts-no-untyped-obj-literals。 */
|
||||||
|
const query: Record<string, string> = {};
|
||||||
|
query['from'] = iso;
|
||||||
|
query['to'] = iso;
|
||||||
|
/* `authProbe: true` ⇒ 401 时**不触发全局"踢回登录页"**(见文件头约束 ③)。
|
||||||
|
⚠️ 代价:它会跳过 `fbApi` 的"自动补令牌",所以**必须显式传 token**。 */
|
||||||
|
const r: FbApiResult = await fbApi('/api/v1/calendar', {
|
||||||
|
token: token,
|
||||||
|
authProbe: true,
|
||||||
|
query: query
|
||||||
|
});
|
||||||
if (r.ok) {
|
if (r.ok) {
|
||||||
applyHome(parseHome(r.data), out);
|
applyRows(parseCalendar(r.data), out, now);
|
||||||
out.state = 'ok';
|
out.state = 'ok';
|
||||||
out.hint = '';
|
out.hint = '';
|
||||||
await saveCache(ctx, r.data);
|
await saveCache(ctx, iso, r.data);
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
const cached: string = await loadCache(ctx);
|
const cached: string = await loadCache(ctx, iso);
|
||||||
if (cached !== '') {
|
if (cached !== '') {
|
||||||
applyHome(parseHome(cached), out);
|
applyRows(parseCalendar(cached), out, now);
|
||||||
out.state = 'offline';
|
out.state = 'offline';
|
||||||
out.hint = r.status === 401 ? '登录已失效,请重新登录' : '离线 · 显示上次的数据';
|
out.hint = r.status === 401 ? '登录已失效,请重新登录' : '离线 · 显示上次的数据';
|
||||||
return out;
|
return out;
|
||||||
@@ -236,10 +393,96 @@ export async function fbCardBuild(ctx: common.Context): Promise<FbCardData> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 供卡片页判断"这条日程是不是第一行"之类的**纯函数**已经不需要了 ——
|
* 时间轴「行」列表:把比例形式的色块换算成 vp 高度,供视图直接 `ForEach`。
|
||||||
* 卡片直接按数组顺序画。这里只留一个给测试/守卫用的口径函数:
|
*
|
||||||
* 卡片上**最多**显示几条日程(大小卡共用同一个上限,避免"两卡数字不一致")。
|
* 为什么不在卡片里算:卡片页的 ArkTS 是受限子集、也不好写测试;
|
||||||
|
* 这里纯函数、只吃标量,守卫与反向验证都能直接盯。
|
||||||
|
*
|
||||||
|
* @param blocks `FbCardBlock[]`(已按 topRatio 升序)
|
||||||
|
* @param startHour 视窗起点小时
|
||||||
|
* @param hourVp 1 小时 = 多少 vp
|
||||||
*/
|
*/
|
||||||
export function fbCardMaxEvents(): number {
|
export function fbCardTlLines(blocks: FbCardBlock[], startHour: number, hourVp: number): FbCardTlLine[] {
|
||||||
return 6;
|
const lines: FbCardTlLine[] = [];
|
||||||
|
// 1) 按冲突组归并(组号在 `FbTimelineUtil.build` 里已算好)
|
||||||
|
const order: number[] = [];
|
||||||
|
const groups: Map<number, FbCardBlock[]> = new Map<number, FbCardBlock[]>();
|
||||||
|
for (const b of blocks) {
|
||||||
|
const arr: FbCardBlock[] | undefined = groups.get(b.groupIndex);
|
||||||
|
if (arr === undefined) {
|
||||||
|
groups.set(b.groupIndex, [b]);
|
||||||
|
order.push(b.groupIndex);
|
||||||
|
} else {
|
||||||
|
arr.push(b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
order.sort((a: number, b: number): number => a - b);
|
||||||
|
|
||||||
|
// 2) 逐组产出:组前补空白 + 组内按列并排
|
||||||
|
// ⚠️ 游标从**视窗起点**开始(不是 0 点):8 点视窗下第一组前面的 0~8 点空白
|
||||||
|
// 不该画进卡片,否则时间轴一开始就有一大段空格子,看着像"数据算错了"。
|
||||||
|
let cursor: number = startHour; // 已画到"当天第几小时"(浮点)
|
||||||
|
for (const gi of order) {
|
||||||
|
const bs: FbCardBlock[] = groups.get(gi) ?? [];
|
||||||
|
if (bs.length === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let top: number = 1;
|
||||||
|
let bot: number = 0;
|
||||||
|
for (const b of bs) {
|
||||||
|
const t: number = b.topRatio * 24;
|
||||||
|
const e: number = (b.topRatio + b.heightRatio) * 24;
|
||||||
|
if (t < top) {
|
||||||
|
top = t;
|
||||||
|
}
|
||||||
|
if (e > bot) {
|
||||||
|
bot = e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (top < cursor) {
|
||||||
|
top = cursor;
|
||||||
|
}
|
||||||
|
const gap: number = (top - cursor) * hourVp;
|
||||||
|
if (gap > 0.5) {
|
||||||
|
const blank: FbCardTlLine = new FbCardTlLine();
|
||||||
|
blank.key = 'gap' + gi.toString();
|
||||||
|
blank.blankVp = gap;
|
||||||
|
lines.push(blank);
|
||||||
|
}
|
||||||
|
const g: FbCardTlGroup = new FbCardTlGroup();
|
||||||
|
g.key = 'g' + gi.toString();
|
||||||
|
g.heightVp = Math.max((bot - top) * hourVp, 14);
|
||||||
|
// 列号可能不连续 ⇒ 用"出现的列号升序"重排,保证 Row 内 layoutWeight 均匀
|
||||||
|
const laneIdx: number[] = [];
|
||||||
|
for (const b of bs) {
|
||||||
|
if (laneIdx.indexOf(b.laneIndex) < 0) {
|
||||||
|
laneIdx.push(b.laneIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
laneIdx.sort((a: number, b: number): number => a - b);
|
||||||
|
for (const li of laneIdx) {
|
||||||
|
const lane: FbCardTlBlock[] = [];
|
||||||
|
for (const b of bs) {
|
||||||
|
if (b.laneIndex !== li) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const cb: FbCardTlBlock = new FbCardTlBlock();
|
||||||
|
cb.key = b.key;
|
||||||
|
cb.title = b.title;
|
||||||
|
cb.timeText = b.timeText;
|
||||||
|
cb.color = b.color;
|
||||||
|
cb.isNow = b.isNow;
|
||||||
|
cb.padTopVp = Math.max((b.topRatio * 24 - top) * hourVp, 0);
|
||||||
|
cb.heightVp = Math.max(b.heightRatio * 24 * hourVp, 14);
|
||||||
|
lane.push(cb);
|
||||||
|
}
|
||||||
|
g.lanes.push(lane);
|
||||||
|
}
|
||||||
|
const line: FbCardTlLine = new FbCardTlLine();
|
||||||
|
line.key = g.key;
|
||||||
|
line.group = g;
|
||||||
|
lines.push(line);
|
||||||
|
cursor = bot;
|
||||||
|
}
|
||||||
|
return lines;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,197 @@
|
|||||||
|
/**
|
||||||
|
* 家书 FamilyBook · 桌面卡片的「每张卡」状态(鸿蒙端)
|
||||||
|
*
|
||||||
|
* 照 `SyncCalendar/entry/src/main/ets/common/CardDataService.ets` 的那一套:
|
||||||
|
* · `registerForm / unregisterForm` —— 记住桌面上有哪些卡(推送时要知道推给谁);
|
||||||
|
* · `getDayOffset / setDayOffset` —— **每张卡各自翻到哪一天**(卡片不能滚到别的日子,
|
||||||
|
* 只能靠翻页;两张卡翻到不同日期是正常的);
|
||||||
|
* · `pushAll` —— 应用内改完日程后,让桌面上的卡立刻跟上。
|
||||||
|
*
|
||||||
|
* 🔴 与 SyncCalendar 的**一处有意加强**:
|
||||||
|
* 它的翻页偏移只放在**内存 Map** 里(进程一被回收就丢),于是系统每 30 分钟的
|
||||||
|
* 定时刷新会把用户刚翻到的"下周三"悄悄拽回"今天"。
|
||||||
|
* 这里落进 `preferences`:FormExtensionAbility 的进程本来就可能随时被回收,
|
||||||
|
* "翻页"这种用户意图必须能跨进程活下来,否则表现就是"翻着翻着自己跳回去了"。
|
||||||
|
*
|
||||||
|
* ⚠️ 偏移量做**夹取**(±366 天):`onFormEvent` 的 message 是外部输入,
|
||||||
|
* 连续点 ‹ 几百次也不该把它算成一个荒唐的年份(`new Date(y, m, d + 99999)`
|
||||||
|
* 在部分设备上会算出 Invalid Date,卡片直接白掉)。
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { common } from '@kit.AbilityKit';
|
||||||
|
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';
|
||||||
|
|
||||||
|
const DOMAIN: number = 0x0000;
|
||||||
|
const TAG: string = 'FbCardForms';
|
||||||
|
|
||||||
|
const STORE_NAME: string = 'fb_card_forms';
|
||||||
|
const K_IDS: string = 'form_ids';
|
||||||
|
const K_OFF: string = 'day_offsets';
|
||||||
|
|
||||||
|
/** 翻页最多到前后一年(防外部输入把日期算成 Invalid Date) */
|
||||||
|
const MAX_OFFSET: number = 366;
|
||||||
|
|
||||||
|
/** 条目分隔符用 `;`、键值用 `=`:formId 是数字串,不会撞这两个字符 */
|
||||||
|
const SEP_ITEM: string = ';';
|
||||||
|
const SEP_KV: string = '=';
|
||||||
|
|
||||||
|
class FbCardFormsImpl {
|
||||||
|
private async store(ctx: common.Context): Promise<preferences.Preferences> {
|
||||||
|
return preferences.getPreferences(ctx, STORE_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async readStr(ctx: common.Context, key: string): Promise<string> {
|
||||||
|
try {
|
||||||
|
const p: preferences.Preferences = await this.store(ctx);
|
||||||
|
const v: preferences.ValueType = await p.get(key, '');
|
||||||
|
return typeof v === 'string' ? v : '';
|
||||||
|
} catch (err) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async writeStr(ctx: common.Context, key: string, val: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const p: preferences.Preferences = await this.store(ctx);
|
||||||
|
await p.put(key, val);
|
||||||
|
// 卡片进程随时可能被回收:不 flush 等于没写
|
||||||
|
await p.flush();
|
||||||
|
} catch (err) {
|
||||||
|
const e: BusinessError = err as BusinessError;
|
||||||
|
hilog.warn(DOMAIN, TAG, '写卡片状态失败(%{public}s):%{public}s', key, e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 偏移量(formId → 相对今天的天数)---- */
|
||||||
|
|
||||||
|
private parseOffsets(raw: string): Map<string, number> {
|
||||||
|
const map: Map<string, number> = new Map<string, number>();
|
||||||
|
for (const item of raw.split(SEP_ITEM)) {
|
||||||
|
if (item === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const at: number = item.indexOf(SEP_KV);
|
||||||
|
if (at <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const id: string = item.substring(0, at);
|
||||||
|
const off: number = parseInt(item.substring(at + 1), 10);
|
||||||
|
if (!isNaN(off)) {
|
||||||
|
map.set(id, off);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
private joinOffsets(map: Map<string, number>): string {
|
||||||
|
const parts: string[] = [];
|
||||||
|
map.forEach((off: number, id: string): void => {
|
||||||
|
parts.push(id + SEP_KV + off.toString());
|
||||||
|
});
|
||||||
|
return parts.join(SEP_ITEM);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 读某张卡翻到第几天(读不到按今天 = 0) */
|
||||||
|
async getOffset(ctx: common.Context, formId: string): Promise<number> {
|
||||||
|
const map: Map<string, number> = this.parseOffsets(await this.readStr(ctx, K_OFF));
|
||||||
|
const v: number | undefined = map.get(formId);
|
||||||
|
return v === undefined ? 0 : v;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 写某张卡的偏移(已夹取到 ±366) */
|
||||||
|
async setOffset(ctx: common.Context, formId: string, off: number): Promise<number> {
|
||||||
|
let v: number = Math.round(off);
|
||||||
|
if (v > MAX_OFFSET) {
|
||||||
|
v = MAX_OFFSET;
|
||||||
|
}
|
||||||
|
if (v < -MAX_OFFSET) {
|
||||||
|
v = -MAX_OFFSET;
|
||||||
|
}
|
||||||
|
const map: Map<string, number> = this.parseOffsets(await this.readStr(ctx, K_OFF));
|
||||||
|
map.set(formId, v);
|
||||||
|
await this.writeStr(ctx, K_OFF, this.joinOffsets(map));
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearOffset(ctx: common.Context, formId: string): Promise<void> {
|
||||||
|
const map: Map<string, number> = this.parseOffsets(await this.readStr(ctx, K_OFF));
|
||||||
|
if (map.delete(formId)) {
|
||||||
|
await this.writeStr(ctx, K_OFF, this.joinOffsets(map));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 卡片注册表 ---- */
|
||||||
|
|
||||||
|
private async ids(ctx: common.Context): Promise<string[]> {
|
||||||
|
const raw: string = await this.readStr(ctx, K_IDS);
|
||||||
|
const out: string[] = [];
|
||||||
|
for (const id of raw.split(SEP_ITEM)) {
|
||||||
|
if (id !== '' && out.indexOf(id) < 0) {
|
||||||
|
out.push(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async saveIds(ctx: common.Context, list: string[]): Promise<void> {
|
||||||
|
await this.writeStr(ctx, K_IDS, list.join(SEP_ITEM));
|
||||||
|
}
|
||||||
|
|
||||||
|
async register(ctx: common.Context, formId: string): Promise<void> {
|
||||||
|
if (formId === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const list: string[] = await this.ids(ctx);
|
||||||
|
if (list.indexOf(formId) < 0) {
|
||||||
|
list.push(formId);
|
||||||
|
await this.saveIds(ctx, list);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async unregister(ctx: common.Context, formId: string): Promise<void> {
|
||||||
|
const list: string[] = (await this.ids(ctx)).filter((id: string): boolean => id !== formId);
|
||||||
|
await this.saveIds(ctx, list);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用内日程变化后刷新**所有**已添加的卡。
|
||||||
|
*
|
||||||
|
* ⚠️ 按偏移量分组构建:两张卡可能翻在不同日期,逐张构建会重复打同一个接口。
|
||||||
|
* ⚠️ 推失败的 formId 直接**注销**(卡片多半已被用户删掉),否则每刷一次都白推一遍。
|
||||||
|
*/
|
||||||
|
async pushAll(ctx: common.Context): Promise<void> {
|
||||||
|
const list: string[] = await this.ids(ctx);
|
||||||
|
if (list.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const cache: Map<number, formBindingData.FormBindingData> =
|
||||||
|
new Map<number, formBindingData.FormBindingData>();
|
||||||
|
const stale: string[] = [];
|
||||||
|
for (const formId of list) {
|
||||||
|
const off: number = await this.getOffset(ctx, formId);
|
||||||
|
let binding: formBindingData.FormBindingData | undefined = cache.get(off);
|
||||||
|
if (binding === undefined) {
|
||||||
|
const data: FbCardData = await fbCardBuild(ctx, off);
|
||||||
|
binding = formBindingData.createFormBindingData(data);
|
||||||
|
cache.set(off, binding);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await formProvider.updateForm(formId, binding);
|
||||||
|
} catch (err) {
|
||||||
|
const e: BusinessError = err as BusinessError;
|
||||||
|
hilog.warn(DOMAIN, TAG, '卡片 %{public}s 推送失败(已注销):%{public}s', formId, e.message);
|
||||||
|
stale.push(formId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (stale.length > 0) {
|
||||||
|
const keep: string[] = list.filter((id: string): boolean => stale.indexOf(id) < 0);
|
||||||
|
await this.saveIds(ctx, keep);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FB_CARD_FORMS: FbCardFormsImpl = new FbCardFormsImpl();
|
||||||
@@ -13,7 +13,7 @@ import { FB_MODULE_TOKEN } from '../data/FbModuleRepo';
|
|||||||
import { FB_KEY_STORE } from '../data/FbKeyStore';
|
import { FB_KEY_STORE } from '../data/FbKeyStore';
|
||||||
import { FB_ATTACH } from '../data/FbAttachQueue';
|
import { FB_ATTACH } from '../data/FbAttachQueue';
|
||||||
import { FB_MODULES } from '../model/Modules';
|
import { FB_MODULES } from '../model/Modules';
|
||||||
import { fbAppState } from '../common/Store';
|
import { fbAppState, FbAppState } from '../common/Store';
|
||||||
|
|
||||||
const DOMAIN: number = 0x0000;
|
const DOMAIN: number = 0x0000;
|
||||||
|
|
||||||
@@ -126,8 +126,30 @@ export default class EntryAbility extends UIAbility {
|
|||||||
if (p === undefined) {
|
if (p === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
/* ---- ① 桌面日程卡片带来的"看哪一天 / 要不要直接新建" ----
|
||||||
|
卡片按 SyncCalendar 那套传 `openDate`(**毫秒字符串**,数字会溢出 int32)
|
||||||
|
与 `widgetAction`(`add` = 点的是卡片上的 +)。
|
||||||
|
⚠️ 日期解析失败就当没传:宁可落到"今天",也不能拿一个 0 去算 ——
|
||||||
|
`new Date(0)` 是 1970 年,用户会看到一屏空白。 */
|
||||||
|
const dateRaw: Object | undefined = p['openDate'];
|
||||||
|
const dateStr: string = typeof dateRaw === 'string' ? dateRaw : '';
|
||||||
|
const actRaw: Object | undefined = p['widgetAction'];
|
||||||
|
const act: string = typeof actRaw === 'string' ? actRaw : '';
|
||||||
|
let jumpIso: string = '';
|
||||||
|
let wantCreate: boolean = act === 'add';
|
||||||
|
if (dateStr !== '') {
|
||||||
|
const ms: number = Number(dateStr);
|
||||||
|
if (!isNaN(ms) && ms > 0) {
|
||||||
|
jumpIso = this.isoOf(new Date(ms));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const raw: Object | undefined = p['fbModule'];
|
const raw: Object | undefined = p['fbModule'];
|
||||||
const key: string = typeof raw === 'string' ? raw : '';
|
let key: string = typeof raw === 'string' ? raw : '';
|
||||||
|
// 卡片只给了日期/动作(没写模块名)⇒ 落日程,与 SyncCalendar 的语义一致
|
||||||
|
if (key === '' && (jumpIso !== '' || wantCreate)) {
|
||||||
|
key = 'calendar';
|
||||||
|
}
|
||||||
if (key === '') {
|
if (key === '') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -142,10 +164,28 @@ export default class EntryAbility extends UIAbility {
|
|||||||
hilog.warn(DOMAIN, 'FamilyBook', '卡片带来的模块名不认识(已忽略):%{public}s', key);
|
hilog.warn(DOMAIN, 'FamilyBook', '卡片带来的模块名不认识(已忽略):%{public}s', key);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
/* ---- ② 把"看哪一天"交给日程页 ----
|
||||||
|
冷启动时日程页还没建,先写进全局状态(页面 `aboutToAppear` 会读);
|
||||||
|
热启动(应用已开着)走 `onNewWant`,页面不会重建 ⇒ 靠 `calJumpSeq` 自增
|
||||||
|
触发页面上的 `@Monitor`。两种入口共用同一份状态,不会"有时跳有时不跳"。 */
|
||||||
|
if (jumpIso !== '' || wantCreate) {
|
||||||
|
const st: FbAppState = fbAppState();
|
||||||
|
st.calJumpIso = jumpIso;
|
||||||
|
st.calJumpCreate = wantCreate;
|
||||||
|
st.calJumpSeq = st.calJumpSeq + 1;
|
||||||
|
hilog.info(DOMAIN, 'FamilyBook', '卡片请求日程:%{public}s(新建=%{public}s)',
|
||||||
|
jumpIso === '' ? '今天' : jumpIso, wantCreate ? '是' : '否');
|
||||||
|
}
|
||||||
fbAppState().switchModule(key);
|
fbAppState().switchModule(key);
|
||||||
hilog.info(DOMAIN, 'FamilyBook', '从卡片进入模块:%{public}s', key);
|
hilog.info(DOMAIN, 'FamilyBook', '从卡片进入模块:%{public}s', key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** `Date` → `YYYY-MM-DD`(本地时区;与卡片数据层同一个口径) */
|
||||||
|
private isoOf(d: Date): string {
|
||||||
|
const p2 = (v: number): string => v < 10 ? '0' + v.toString() : v.toString();
|
||||||
|
return d.getFullYear().toString() + '-' + p2(d.getMonth() + 1) + '-' + p2(d.getDate());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 应用**已经在运行**时再从卡片点进来(热启动)走这里。
|
* 应用**已经在运行**时再从卡片点进来(热启动)走这里。
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,22 +1,23 @@
|
|||||||
/**
|
/**
|
||||||
* 家书 FamilyBook · 服务卡片提供方(鸿蒙端 FormExtensionAbility)
|
* 家书 FamilyBook · 服务卡片提供方(鸿蒙端 FormExtensionAbility)
|
||||||
*
|
*
|
||||||
* 用户 2026-10-20:「做几个和 syncCalendar 差不多的卡片。」
|
* 用户 2026-10-20:「跟日程有关的桌面服务卡片,你就完全按照 synccalendar 来做就行。」
|
||||||
* 参照 `SyncCalendar` 的 `EntryFormAbility`:
|
* ⇒ 生命周期与翻页照 `SyncCalendar/entry/src/main/ets/entryformability/EntryFormAbility.ets`:
|
||||||
* · `onAddForm` —— 添加到桌面时先塞一份"空壳",再异步推真实数据;
|
* · `onAddForm` —— 添加到桌面时先塞"空壳",再异步推真实数据,并**登记**这张卡;
|
||||||
* · `onUpdateForm` —— 系统定时刷新(`form_config.json` 里 `updateDuration: 1` ⇒ 30 分钟);
|
* · `onUpdateForm` —— 系统定时刷新(`updateDuration: 1` ⇒ 30 分钟);
|
||||||
* · `onFormEvent` —— 卡片上的「刷新」按钮(`action: 'message'`);
|
* · `onFormEvent` —— 卡片上的 ‹ / › / 今 三个按钮(`action: 'message'`);
|
||||||
* · `onRemoveForm` —— 移出桌面。
|
* · `onRemoveForm` —— 移出桌面(注销 + 清掉它的翻页偏移)。
|
||||||
*
|
*
|
||||||
* 🔴 三个必须记住的约束:
|
* 🔴 四个必须记住的约束:
|
||||||
* ① **生命周期回调返回后进程只活约 10 秒**(官方文档),所以回调里**不能做长任务**。
|
* ① **生命周期回调返回后进程只活约 10 秒**(官方文档)⇒ 回调里不能做长任务。
|
||||||
* 这里的做法与 SyncCalendar 一致:立刻返回/用本地缓存,真实的网络请求
|
* 这里的做法与 SyncCalendar 一致:立刻返回/用本地缓存,真实网络请求放在
|
||||||
* 放在 `.then()` 里异步推 —— 就算被回收,也只是"这次没刷成",
|
* `.then()` 里异步推 —— 就算被回收,也只是"这次没刷成",30 分钟后还会重试。
|
||||||
* 下一次系统刷新(30 分钟)还会重试。**卡片数据不是关键数据,允许慢一拍。**
|
* ② `onAddForm` **必须同步返回** `FormBindingData`(不能是 Promise),否则桌面
|
||||||
* ② `onAddForm` **必须同步返回**一个 `FormBindingData`(不能是 Promise),
|
* 直接显示"卡片加载失败"。
|
||||||
* 否则桌面直接显示"卡片加载失败"。
|
* ③ 数据构建失败**绝不能抛**:抛出去的表现是桌面一张白卡,用户完全不知道发生了什么。
|
||||||
* ③ 数据构建失败**绝不能抛**:抛出去的表现是桌面上一张白卡,
|
* ⇒ 失败也返回"带一句人话"的壳(`FbCardData.hint`)。
|
||||||
* 用户完全不知道发生了什么。⇒ 失败也返回"带一句人话"的壳(见 `FbCardData.hint`)。
|
* ④ **翻页 = 每张卡一份偏移量**(`FB_CARD_FORMS`),推数据时按各自偏移构建 ——
|
||||||
|
* 系统定时刷新**不能**把用户翻走的日期拽回今天。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { FormExtensionAbility, formBindingData, formProvider } from '@kit.FormKit';
|
import { FormExtensionAbility, formBindingData, formProvider } from '@kit.FormKit';
|
||||||
@@ -24,6 +25,7 @@ import { Want } from '@kit.AbilityKit';
|
|||||||
import { BusinessError } from '@kit.BasicServicesKit';
|
import { BusinessError } from '@kit.BasicServicesKit';
|
||||||
import { hilog } from '@kit.PerformanceAnalysisKit';
|
import { hilog } from '@kit.PerformanceAnalysisKit';
|
||||||
import { FbCardData, fbCardBuild } from '../data/FbCardData';
|
import { FbCardData, fbCardBuild } from '../data/FbCardData';
|
||||||
|
import { FB_CARD_FORMS } from '../data/FbCardForms';
|
||||||
|
|
||||||
const DOMAIN: number = 0x0000;
|
const DOMAIN: number = 0x0000;
|
||||||
const TAG: string = 'FbForm';
|
const TAG: string = 'FbForm';
|
||||||
@@ -32,14 +34,14 @@ export default class FbFormAbility extends FormExtensionAbility {
|
|||||||
/**
|
/**
|
||||||
* 重新算一遍并推给指定卡片。
|
* 重新算一遍并推给指定卡片。
|
||||||
*
|
*
|
||||||
* ⚠️ 三种尺寸(2×2 / 2×4 / 4×4)共用这一份数据:多算一次就多打一次接口,
|
* ⚠️ 四块卡(2×2 / 2×4 / 4×4 / 6×4)共用这份数据:它们只是**排版不同**,
|
||||||
* 没有必要 —— 它们只是**排版不同**,数字必须是同一个。
|
* 数字与条目必须是同一个 —— 多算一遍既慢又可能口径不一致。
|
||||||
*/
|
*/
|
||||||
private push(formId: string): void {
|
private push(formId: string, dayOffset: number): void {
|
||||||
fbCardBuild(this.context).then((data: FbCardData): void => {
|
fbCardBuild(this.context, dayOffset).then((data: FbCardData): void => {
|
||||||
const binding: formBindingData.FormBindingData = formBindingData.createFormBindingData(data);
|
const binding: formBindingData.FormBindingData = formBindingData.createFormBindingData(data);
|
||||||
formProvider.updateForm(formId, binding).then((): void => {
|
formProvider.updateForm(formId, binding).then((): void => {
|
||||||
hilog.info(DOMAIN, TAG, '卡片 %{public}s 已更新(%{public}s)', formId, data.state);
|
hilog.info(DOMAIN, TAG, '卡片 %{public}s 已更新(偏移 %{public}d)', formId, data.dayOffset);
|
||||||
}).catch((err: BusinessError): void => {
|
}).catch((err: BusinessError): void => {
|
||||||
// 卡片可能已经被用户删了 —— 打一条日志就够,不要重试
|
// 卡片可能已经被用户删了 —— 打一条日志就够,不要重试
|
||||||
hilog.warn(DOMAIN, TAG, '卡片 %{public}s 更新失败:%{public}s', formId, err.message);
|
hilog.warn(DOMAIN, TAG, '卡片 %{public}s 更新失败:%{public}s', formId, err.message);
|
||||||
@@ -62,10 +64,14 @@ export default class FbFormAbility extends FormExtensionAbility {
|
|||||||
const formId: string = this.formIdOf(want);
|
const formId: string = this.formIdOf(want);
|
||||||
hilog.info(DOMAIN, TAG, '添加卡片:%{public}s', formId);
|
hilog.info(DOMAIN, TAG, '添加卡片:%{public}s', formId);
|
||||||
if (formId !== '') {
|
if (formId !== '') {
|
||||||
// 异步补真实数据(不 await:见文件头约束 ①)
|
// 异步:登记 + 补真实数据(不 await:见文件头约束 ①)
|
||||||
this.push(formId);
|
FB_CARD_FORMS.register(this.context, formId).then((): void => {
|
||||||
|
this.push(formId, 0);
|
||||||
|
}).catch((err: BusinessError): void => {
|
||||||
|
hilog.warn(DOMAIN, TAG, '卡片登记失败:%{public}s', err.message);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
// 先给一份"正在取"的壳 —— 卡片上会显示这天/这周的空状态,而不是白屏
|
// 先给一份"正在取"的壳 —— 卡片上会显示空态,而不是白屏
|
||||||
const shell: FbCardData = new FbCardData();
|
const shell: FbCardData = new FbCardData();
|
||||||
shell.state = 'loading';
|
shell.state = 'loading';
|
||||||
shell.hint = '正在取数据…';
|
shell.hint = '正在取数据…';
|
||||||
@@ -74,7 +80,13 @@ export default class FbFormAbility extends FormExtensionAbility {
|
|||||||
|
|
||||||
onUpdateForm(formId: string): void {
|
onUpdateForm(formId: string): void {
|
||||||
hilog.info(DOMAIN, TAG, '系统定时刷新:%{public}s', formId);
|
hilog.info(DOMAIN, TAG, '系统定时刷新:%{public}s', formId);
|
||||||
this.push(formId);
|
// ⚠️ 必须按**这张卡自己的偏移**重建,否则定时刷新会把用户翻走的日期拽回今天
|
||||||
|
FB_CARD_FORMS.getOffset(this.context, formId).then((off: number): void => {
|
||||||
|
this.push(formId, off);
|
||||||
|
}).catch((err: BusinessError): void => {
|
||||||
|
hilog.warn(DOMAIN, TAG, '读卡片偏移失败:%{public}s', err.message);
|
||||||
|
this.push(formId, 0);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onFormEvent(formId: string, message: string): void {
|
onFormEvent(formId: string, message: string): void {
|
||||||
@@ -82,11 +94,33 @@ export default class FbFormAbility extends FormExtensionAbility {
|
|||||||
⚠️ 不解析 JSON:不同版本系统给的形态可能是 `{params:{...}}` 或直接 `{...}`,
|
⚠️ 不解析 JSON:不同版本系统给的形态可能是 `{params:{...}}` 或直接 `{...}`,
|
||||||
子串匹配更稳(与 SyncCalendar 同一个取舍)。 */
|
子串匹配更稳(与 SyncCalendar 同一个取舍)。 */
|
||||||
hilog.info(DOMAIN, TAG, '卡片事件:%{public}s %{public}s', formId, message);
|
hilog.info(DOMAIN, TAG, '卡片事件:%{public}s %{public}s', formId, message);
|
||||||
this.push(formId);
|
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, off);
|
||||||
|
}).catch((err: BusinessError): void => {
|
||||||
|
hilog.warn(DOMAIN, TAG, '卡片翻页失败:%{public}s', err.message);
|
||||||
|
this.push(formId, cur);
|
||||||
|
});
|
||||||
|
}).catch((err: BusinessError): void => {
|
||||||
|
hilog.warn(DOMAIN, TAG, '读卡片偏移失败(按今天处理):%{public}s', err.message);
|
||||||
|
this.push(formId, 0);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onRemoveForm(formId: string): void {
|
onRemoveForm(formId: string): void {
|
||||||
// 本卡片不落任何"每张卡"的状态(没有翻页偏移之类),所以只需记一条日志
|
|
||||||
hilog.info(DOMAIN, TAG, '移除卡片:%{public}s', formId);
|
hilog.info(DOMAIN, TAG, '移除卡片:%{public}s', formId);
|
||||||
|
FB_CARD_FORMS.unregister(this.context, formId).then((): Promise<void> => {
|
||||||
|
return FB_CARD_FORMS.clearOffset(this.context, formId);
|
||||||
|
}).catch((err: BusinessError): void => {
|
||||||
|
hilog.warn(DOMAIN, TAG, '注销卡片失败:%{public}s', err.message);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,12 +24,16 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { FbColors, FbMetric, fbPalette, FbTint, FB_MASK } from '../../common/Theme';
|
import { FbColors, FbMetric, fbPalette, FbTint, FB_MASK } from '../../common/Theme';
|
||||||
|
import { common } from '@kit.AbilityKit';
|
||||||
|
import { BusinessError } from '@kit.BasicServicesKit';
|
||||||
|
import { hilog } from '@kit.PerformanceAnalysisKit';
|
||||||
import { FbAppState, fbAppState } from '../../common/Store';
|
import { FbAppState, fbAppState } from '../../common/Store';
|
||||||
import { FbCalBook, FbCalEvent, isEditableEvent } from '../../model/Types';
|
import { FbCalBook, FbCalEvent, isEditableEvent } from '../../model/Types';
|
||||||
import { parseCalendar } from '../../data/FbParse';
|
import { parseCalendar } from '../../data/FbParse';
|
||||||
import { loadModule } from '../../data/FbModuleLoad';
|
import { loadModule } from '../../data/FbModuleLoad';
|
||||||
import { FbCalRepo, FbCalEventFull } from '../../data/FbCalRepo';
|
import { FbCalRepo, FbCalEventFull } from '../../data/FbCalRepo';
|
||||||
import { FB_CAL_SETTINGS, FbCalView, FbCalStyle } from '../../data/FbCalSettings';
|
import { FB_CAL_SETTINGS, FbCalView, FbCalStyle } from '../../data/FbCalSettings';
|
||||||
|
import { FB_CARD_FORMS } from '../../data/FbCardForms';
|
||||||
import { FB_TOKEN_STORE } from '../../data/FbTokenStore';
|
import { FB_TOKEN_STORE } from '../../data/FbTokenStore';
|
||||||
import { FbEmptyBox, FbIconButton } from '../../components/Widgets';
|
import { FbEmptyBox, FbIconButton } from '../../components/Widgets';
|
||||||
import { FbEventSheet } from '../../components/FbEventSheet';
|
import { FbEventSheet } from '../../components/FbEventSheet';
|
||||||
@@ -288,10 +292,45 @@ export struct FbCalendarPage {
|
|||||||
this.mode = FB_CAL_SETTINGS.getView();
|
this.mode = FB_CAL_SETTINGS.getView();
|
||||||
this.style = FB_CAL_SETTINGS.getStyle();
|
this.style = FB_CAL_SETTINGS.getStyle();
|
||||||
this.pickedIso = this.todayIso();
|
this.pickedIso = this.todayIso();
|
||||||
|
// 桌面日程卡片可能指定了"看哪一天 / 直接新建"(冷启动入口)
|
||||||
|
this.applyCardJump();
|
||||||
this.load();
|
this.load();
|
||||||
this.loadBooks();
|
this.loadBooks();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 桌面日程卡片点进来:定位到卡片显示的那一天;若是点「+」进来的,直接开新建浮层。
|
||||||
|
*
|
||||||
|
* 🔴 两种入口都要照顾(用户 2026-10-20 要求"完全照 SyncCalendar"):
|
||||||
|
* · 冷启动 —— 页面 `aboutToAppear` 时读一次;
|
||||||
|
* · 热启动 —— 应用已经开着,`onNewWant` 只改了全局状态、页面不会重建
|
||||||
|
* ⇒ 靠下面那个 `@Monitor` 盯住请求序号,再触发一次。
|
||||||
|
* ⚠️ `calJumpIso` 为空表示"卡片只点了 +,没指定日期" ⇒ 落到今天(不要清空 pickedIso)。
|
||||||
|
*/
|
||||||
|
private applyCardJump(): void {
|
||||||
|
const iso: string = this.app.calJumpIso;
|
||||||
|
if (iso.length === 10) {
|
||||||
|
this.pickedIso = iso;
|
||||||
|
const y: number = parseInt(iso.substring(0, 4), 10);
|
||||||
|
const m: number = parseInt(iso.substring(5, 7), 10);
|
||||||
|
if (!isNaN(y) && !isNaN(m) && m >= 1 && m <= 12) {
|
||||||
|
this.showYear = y;
|
||||||
|
this.showMonth = m;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.app.calJumpCreate) {
|
||||||
|
// 用掉就清掉:否则用户关掉新建浮层后,每次回到本页又会自己弹出来
|
||||||
|
this.app.calJumpCreate = false;
|
||||||
|
this.creating = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 卡片在应用已开着时被点(`onNewWant` 改了全局状态)→ 这里响应 */
|
||||||
|
@Monitor('app.calJumpSeq')
|
||||||
|
onCardJump(): void {
|
||||||
|
this.applyCardJump();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 四个浮层(详情 / 编辑 / 新建 / 设置)任一开着 → 让底部悬浮快捷栏让位。
|
* 四个浮层(详情 / 编辑 / 新建 / 设置)任一开着 → 让底部悬浮快捷栏让位。
|
||||||
*
|
*
|
||||||
@@ -714,6 +753,24 @@ export struct FbCalendarPage {
|
|||||||
this.creating = false;
|
this.creating = false;
|
||||||
this.load();
|
this.load();
|
||||||
this.loadBooks();
|
this.loadBooks();
|
||||||
|
// 桌面上的日程卡片也要立刻跟上(否则要等系统 30 分钟后的定时刷新)
|
||||||
|
this.pushCards();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 把最新数据推给桌面上所有日程卡片。
|
||||||
|
*
|
||||||
|
* ⚠️ 不 await、失败只记日志:这里是"顺手做的一件好事",
|
||||||
|
* 推送失败(卡片已被用户删掉等)绝不能影响应用内的保存动作。
|
||||||
|
*/
|
||||||
|
private pushCards(): void {
|
||||||
|
const ctx: common.Context | undefined = this.getUIContext().getHostContext();
|
||||||
|
if (ctx === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
FB_CARD_FORMS.pushAll(ctx).catch((err: BusinessError): void => {
|
||||||
|
hilog.warn(0x0000, 'FbCalendar', '刷新桌面卡片失败:%{public}s', err.message);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 详情是不是开着 */
|
/** 详情是不是开着 */
|
||||||
|
|||||||
@@ -1,157 +1,134 @@
|
|||||||
/**
|
/**
|
||||||
* 家书 FamilyBook · 2×2 服务卡片(今日概览)
|
* 家书 FamilyBook · 2×2 服务卡片(日程 · 当天概览)
|
||||||
*
|
*
|
||||||
* 用户 2026-10-20:「做几个和 syncCalendar 差不多的卡片。」
|
* 用户 2026-10-20:「跟日程有关的桌面服务卡片,你就完全按照 synccalendar 来做就行。」
|
||||||
|
* ⇒ 照 `SyncCalendar/entry/src/main/ets/pages/widget/Widget2x2.ets`:
|
||||||
|
* 日期(月日)+ 星期/农历 + 当天日程条数大数字;右上角 + 直接新建日程;
|
||||||
|
* **整卡可点**(空白处也能进应用,并定位到卡片显示的那一天)。
|
||||||
*
|
*
|
||||||
* 这块卡只回答一句话:**今天有几件事、最要紧的那件是什么**。
|
* ⚠️ 动态卡片**不能自己发网络请求**:这里只画 `FbCardData` 推过来的字段,
|
||||||
* 所以版面是「日期 → 大数字 → 一行提示」,不做列表(2×2 放列表只会挤成蚂蚁)。
|
* 要重新取数只能 `postCardAction({action:'message'})` 交给 `FbFormAbility`。
|
||||||
*
|
|
||||||
* ⚠️ 卡片里**不做任何计算**(不排序、不筛选、不拼日期):
|
|
||||||
* 全部由 `data/FbCardData.ets` 算好塞进 `LocalStorage`(原因见那边的文件头)。
|
|
||||||
* 这里只负责画 —— 三种尺寸共用同一份数据,数字必须一致。
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { FbCardDay, FbCardEvent } from '../../data/FbCardData';
|
|
||||||
|
|
||||||
let storage2x2 = new LocalStorage();
|
let storage2x2 = new LocalStorage();
|
||||||
|
|
||||||
@Entry(storage2x2)
|
@Entry(storage2x2)
|
||||||
@Component
|
@Component
|
||||||
struct FbCard2x2 {
|
struct FbCard2x2 {
|
||||||
@LocalStorageProp('state') state: string = 'loading';
|
@LocalStorageProp('state') state: string = 'loading';
|
||||||
@LocalStorageProp('todayLabel') todayLabel: string = '';
|
|
||||||
@LocalStorageProp('weekday') weekday: string = '';
|
|
||||||
@LocalStorageProp('lunarShort') lunarShort: string = '';
|
|
||||||
@LocalStorageProp('marks') marks: string = '';
|
|
||||||
@LocalStorageProp('eventCount') eventCount: number = 0;
|
|
||||||
@LocalStorageProp('doing') doing: number = 0;
|
|
||||||
@LocalStorageProp('todo') todo: number = 0;
|
|
||||||
@LocalStorageProp('overdue') overdue: number = 0;
|
|
||||||
@LocalStorageProp('eventsJson') eventsJson: string = '[]';
|
|
||||||
@LocalStorageProp('daysJson') daysJson: string = '[]';
|
|
||||||
@LocalStorageProp('hint') hint: string = '';
|
@LocalStorageProp('hint') hint: string = '';
|
||||||
@LocalStorageProp('updatedAt') updatedAt: string = '';
|
@LocalStorageProp('dateMd') dateMd: string = '';
|
||||||
|
@LocalStorageProp('dateText') dateText: string = '';
|
||||||
|
@LocalStorageProp('weekday') weekday: string = '';
|
||||||
|
@LocalStorageProp('lunarText') lunarText: string = '';
|
||||||
|
@LocalStorageProp('dayCount') dayCount: number = 0;
|
||||||
|
/** 卡片当前显示那天的 0 点毫秒 —— 点卡片进应用时定位到这一天 */
|
||||||
|
@LocalStorageProp('selectedDate') selectedDate: number = 0;
|
||||||
|
|
||||||
/** 副标题:`星期二 九月初一`(缺哪个就不占位) */
|
/** 副标题:`星期二 九月初一`(缺哪段就少哪段) */
|
||||||
private subLine(): string {
|
private subLine(): string {
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
if (this.weekday !== '') {
|
if (this.weekday !== '') {
|
||||||
parts.push(this.weekday);
|
parts.push(this.weekday);
|
||||||
}
|
}
|
||||||
if (this.lunarShort !== '') {
|
if (this.lunarText !== '') {
|
||||||
parts.push(this.lunarShort);
|
parts.push(this.lunarText);
|
||||||
}
|
}
|
||||||
return parts.join(' ');
|
return parts.join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** 右上角 +:拉起应用直接进「新建日程」 */
|
||||||
* 最下面那一行:优先"最近的纪念日倒计时",其次"今天第一条日程"。
|
@Builder
|
||||||
*
|
addButton() {
|
||||||
* ⚠️ 不显示"共 N 条"这种废话 —— 大数字已经说了。
|
Button() {
|
||||||
*/
|
Text('+')
|
||||||
private footLine(): string {
|
.fontSize(20)
|
||||||
if (this.hint !== '') {
|
.fontWeight(FontWeight.Medium)
|
||||||
return this.hint;
|
.fontColor('#FFFFFF')
|
||||||
}
|
}
|
||||||
const days = JSON.parse(this.daysJson) as FbCardDay[];
|
.width(30)
|
||||||
if (days.length > 0) {
|
.height(30)
|
||||||
const d: FbCardDay = days[0];
|
.borderRadius(15)
|
||||||
return d.days === 0 ? `${d.title} 就是今天` : `${d.title} 还有 ${d.days} 天`;
|
.padding(0)
|
||||||
}
|
.backgroundColor('#A84A28')
|
||||||
const evs = JSON.parse(this.eventsJson) as FbCardEvent[];
|
/* ⚠️ 无需阻止冒泡:ArkUI 的 `onClick` 是**非冒泡**事件,父子同绑时**子组件优先**,
|
||||||
if (evs.length > 0) {
|
故点 + 不会连带触发根容器的"打开日程"。 */
|
||||||
return `${evs[0].time} ${evs[0].title}`;
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 右下角小字:逾期优先(那是要人马上处理的) */
|
|
||||||
private statLine(): string {
|
|
||||||
if (this.overdue > 0) {
|
|
||||||
return `逾期 ${this.overdue}`;
|
|
||||||
}
|
|
||||||
if (this.todo > 0) {
|
|
||||||
return `待办 ${this.todo}`;
|
|
||||||
}
|
|
||||||
return this.updatedAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
build() {
|
|
||||||
Column() {
|
|
||||||
// ---- 抬头:日期 + 日子标记 ----
|
|
||||||
Row() {
|
|
||||||
Column({ space: 1 }) {
|
|
||||||
Text(this.todayLabel === '' ? '家书' : this.todayLabel)
|
|
||||||
.fontSize(14)
|
|
||||||
.fontWeight(FontWeight.Medium)
|
|
||||||
.fontColor('#2C231D')
|
|
||||||
.maxLines(1)
|
|
||||||
Text(this.subLine())
|
|
||||||
.fontSize(10)
|
|
||||||
.fontColor('#A8988A')
|
|
||||||
.maxLines(1)
|
|
||||||
}
|
|
||||||
.alignItems(HorizontalAlign.Start)
|
|
||||||
.layoutWeight(1)
|
|
||||||
|
|
||||||
if (this.marks !== '') {
|
|
||||||
Text(this.marks)
|
|
||||||
.fontSize(9)
|
|
||||||
.fontColor('#A84A28')
|
|
||||||
.maxLines(1)
|
|
||||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
|
||||||
.padding({ left: 4, right: 4, top: 1, bottom: 1 })
|
|
||||||
.backgroundColor('#F7EFE6')
|
|
||||||
.borderRadius(4)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.width('100%')
|
|
||||||
|
|
||||||
Blank()
|
|
||||||
|
|
||||||
// ---- 大数字 ----
|
|
||||||
Row({ space: 6 }) {
|
|
||||||
Text(`${this.eventCount}`)
|
|
||||||
.fontSize(34)
|
|
||||||
.fontWeight(FontWeight.Bold)
|
|
||||||
.fontColor('#A84A28')
|
|
||||||
Column({ space: 1 }) {
|
|
||||||
Text('件日程')
|
|
||||||
.fontSize(11)
|
|
||||||
.fontColor('#2C231D')
|
|
||||||
Text(this.statLine())
|
|
||||||
.fontSize(10)
|
|
||||||
.fontColor(this.overdue > 0 ? '#C0533F' : '#A8988A')
|
|
||||||
}
|
|
||||||
.alignItems(HorizontalAlign.Start)
|
|
||||||
}
|
|
||||||
.width('100%')
|
|
||||||
.alignItems(VerticalAlign.Bottom)
|
|
||||||
|
|
||||||
Blank()
|
|
||||||
|
|
||||||
// ---- 一行提示 ----
|
|
||||||
Text(this.footLine())
|
|
||||||
.fontSize(10)
|
|
||||||
.fontColor(this.hint !== '' ? '#C0533F' : '#7E6B5A')
|
|
||||||
.maxLines(1)
|
|
||||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
|
||||||
.width('100%')
|
|
||||||
}
|
|
||||||
.width('100%')
|
|
||||||
.height('100%')
|
|
||||||
.padding(12)
|
|
||||||
.backgroundColor('#FFFFFF')
|
|
||||||
.borderRadius(16)
|
|
||||||
.alignItems(HorizontalAlign.Start)
|
|
||||||
// 整卡点击 → 打开日程(这张卡的主角就是"今天有几件事")
|
|
||||||
.onClick((): void => {
|
.onClick((): void => {
|
||||||
postCardAction(this, {
|
postCardAction(this, {
|
||||||
action: 'router',
|
action: 'router',
|
||||||
abilityName: 'EntryAbility',
|
abilityName: 'EntryAbility',
|
||||||
params: { fbModule: 'calendar' }
|
params: { fbModule: 'calendar', widgetAction: 'add', openDate: `${this.selectedDate}` }
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
/** 打开应用并**定位到卡片当前显示的那一天** */
|
||||||
|
private openApp(): void {
|
||||||
|
postCardAction(this, {
|
||||||
|
action: 'router',
|
||||||
|
abilityName: 'EntryAbility',
|
||||||
|
// 必须转成字符串:param 的数值按 int32 传递,13 位毫秒时间戳会溢出(SyncCalendar 踩过)
|
||||||
|
params: { fbModule: 'calendar', openDate: `${this.selectedDate}` }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
build() {
|
||||||
|
Column({ space: 6 }) {
|
||||||
|
// ---- 抬头:日期 + + ----
|
||||||
|
Row({ space: 6 }) {
|
||||||
|
Column({ space: 2 }) {
|
||||||
|
Text(this.dateMd === '' ? this.dateText : this.dateMd)
|
||||||
|
.fontSize(15)
|
||||||
|
.fontWeight(FontWeight.Bold)
|
||||||
|
.fontColor('#2C231D')
|
||||||
|
.maxLines(1)
|
||||||
|
Text(this.subLine())
|
||||||
|
.fontSize(11)
|
||||||
|
.fontColor('#A8988A')
|
||||||
|
.maxLines(1)
|
||||||
|
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||||
|
}
|
||||||
|
.layoutWeight(1)
|
||||||
|
.alignItems(HorizontalAlign.Start)
|
||||||
|
|
||||||
|
this.addButton()
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
.strokeWidth(0.5)
|
||||||
|
.color('#EFE6D8')
|
||||||
|
|
||||||
|
// ---- 当天日程条数(占满剩余空间,垂直居中)----
|
||||||
|
Row({ space: 8 }) {
|
||||||
|
Text(`${this.dayCount}`)
|
||||||
|
.fontSize(40)
|
||||||
|
.fontWeight(FontWeight.Bold)
|
||||||
|
.fontColor('#A84A28')
|
||||||
|
Column({ space: 2 }) {
|
||||||
|
Text('今日日程')
|
||||||
|
.fontSize(12)
|
||||||
|
.fontColor('#2C231D')
|
||||||
|
.maxLines(1)
|
||||||
|
Text(this.hint !== '' ? this.hint : (this.dayCount === 0 ? '点击添加' : '点击查看'))
|
||||||
|
.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)
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.height('100%')
|
||||||
|
.padding(14)
|
||||||
|
.backgroundColor('#FFFFFF')
|
||||||
|
.borderRadius(16)
|
||||||
|
// ⭐ 整卡可点:空白处也能进应用(原来只有日期区/条数区可点)
|
||||||
|
.onClick((): void => this.openApp())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,162 +1,227 @@
|
|||||||
/**
|
/**
|
||||||
* 家书 FamilyBook · 2×4 服务卡片(今日日程 · 左右分栏)
|
* 家书 FamilyBook · 2×4 服务卡片(日程 · 左概览 / 右待办)
|
||||||
*
|
*
|
||||||
* 用户 2026-10-20:「做几个和 syncCalendar 差不多的卡片。」
|
* 用户 2026-10-20:「跟日程有关的桌面服务卡片,你就完全按照 synccalendar 来做就行。」
|
||||||
|
* ⇒ 照 `SyncCalendar/entry/src/main/ets/pages/widget/Widget4x2.ets`:
|
||||||
|
* 左半 = 2×2 那套(日期 / 星期农历 / 当天条数);右半 = **接下来两条**日程;
|
||||||
|
* 整卡可点进应用并定位到当天,右上角 + 直接新建。
|
||||||
*
|
*
|
||||||
* 左边一列是"今天怎么样"(日期 / 农历 / 件数 / 待办),右边是**今天的前三条日程**。
|
* ⚠️ 与 2×2 / 4×4 / 6×4 **同一份数据**(`data/FbCardData.ets` 算一次,四卡共用),
|
||||||
* 2×2 只能给个数字,说不出"是什么事";这块卡就是补那一步。
|
* 所以条数一定一致 —— 不要在卡片里自己算。
|
||||||
*
|
|
||||||
* ⚠️ 与 2×2 / 4×4 **同一份数据**(`data/FbCardData.ets` 算一次,三卡共用),
|
|
||||||
* 所以件数一定一致 —— 不要在卡片里自己算。
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { FbCardDay, FbCardEvent } from '../../data/FbCardData';
|
|
||||||
|
|
||||||
let storage2x4 = new LocalStorage();
|
let storage2x4 = new LocalStorage();
|
||||||
|
|
||||||
|
/** 右半「接下来两条」(与 `listJson` 同结构,这里只用到这几个字段) */
|
||||||
|
class FbCardUp {
|
||||||
|
title: string = '';
|
||||||
|
time: string = '';
|
||||||
|
endTime: string = '';
|
||||||
|
color: string = '#A84A28';
|
||||||
|
isNow: boolean = false;
|
||||||
|
}
|
||||||
|
|
||||||
@Entry(storage2x4)
|
@Entry(storage2x4)
|
||||||
@Component
|
@Component
|
||||||
struct FbCard2x4 {
|
struct FbCard2x4 {
|
||||||
@LocalStorageProp('state') state: string = 'loading';
|
@LocalStorageProp('state') state: string = 'loading';
|
||||||
@LocalStorageProp('todayLabel') todayLabel: string = '';
|
|
||||||
@LocalStorageProp('weekday') weekday: string = '';
|
|
||||||
@LocalStorageProp('lunarShort') lunarShort: string = '';
|
|
||||||
@LocalStorageProp('eventCount') eventCount: number = 0;
|
|
||||||
@LocalStorageProp('doing') doing: number = 0;
|
|
||||||
@LocalStorageProp('todo') todo: number = 0;
|
|
||||||
@LocalStorageProp('overdue') overdue: number = 0;
|
|
||||||
@LocalStorageProp('eventsJson') eventsJson: string = '[]';
|
|
||||||
@LocalStorageProp('hint') hint: string = '';
|
@LocalStorageProp('hint') hint: string = '';
|
||||||
|
@LocalStorageProp('dateMd') dateMd: string = '';
|
||||||
|
@LocalStorageProp('dateText') dateText: string = '';
|
||||||
|
@LocalStorageProp('weekday') weekday: string = '';
|
||||||
|
@LocalStorageProp('lunarText') lunarText: string = '';
|
||||||
|
@LocalStorageProp('dayCount') dayCount: number = 0;
|
||||||
|
@LocalStorageProp('upcomingJson') upcomingJson: string = '[]';
|
||||||
|
@LocalStorageProp('selectedDate') selectedDate: number = 0;
|
||||||
|
|
||||||
|
/** 副标题:`星期二 九月初一` */
|
||||||
private subLine(): string {
|
private subLine(): string {
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
if (this.weekday !== '') {
|
if (this.weekday !== '') {
|
||||||
parts.push(this.weekday);
|
parts.push(this.weekday);
|
||||||
}
|
}
|
||||||
if (this.lunarShort !== '') {
|
if (this.lunarText !== '') {
|
||||||
parts.push(this.lunarShort);
|
parts.push(this.lunarText);
|
||||||
}
|
}
|
||||||
return parts.join(' ');
|
return parts.join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 右侧最多三条(再多 2×4 的高度放不下,会被裁掉半行) */
|
/** 右半最多两行(数据侧已裁剪,这里再兜底截断) */
|
||||||
private top3(): FbCardEvent[] {
|
private upcoming(): FbCardUp[] {
|
||||||
const all = JSON.parse(this.eventsJson) as FbCardEvent[];
|
try {
|
||||||
const out: FbCardEvent[] = [];
|
const all: FbCardUp[] = JSON.parse(this.upcomingJson) as FbCardUp[];
|
||||||
for (let i = 0; i < all.length && i < 3; i++) {
|
return all.slice(0, 2);
|
||||||
out.push(all[i]);
|
} catch (err) {
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
return out;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
build() {
|
@Builder
|
||||||
Row({ space: 10 }) {
|
addButton() {
|
||||||
// ---- 左栏:今天的"账" ----
|
Button() {
|
||||||
Column({ space: 2 }) {
|
Text('+')
|
||||||
Text(this.todayLabel === '' ? '家书' : this.todayLabel)
|
.fontSize(18)
|
||||||
.fontSize(15)
|
.fontWeight(FontWeight.Medium)
|
||||||
.fontWeight(FontWeight.Medium)
|
.fontColor('#FFFFFF')
|
||||||
.fontColor('#2C231D')
|
|
||||||
.maxLines(1)
|
|
||||||
Text(this.subLine())
|
|
||||||
.fontSize(10)
|
|
||||||
.fontColor('#A8988A')
|
|
||||||
.maxLines(1)
|
|
||||||
|
|
||||||
Blank()
|
|
||||||
|
|
||||||
Row({ space: 5 }) {
|
|
||||||
Text(`${this.eventCount}`)
|
|
||||||
.fontSize(30)
|
|
||||||
.fontWeight(FontWeight.Bold)
|
|
||||||
.fontColor('#A84A28')
|
|
||||||
Text('件日程')
|
|
||||||
.fontSize(11)
|
|
||||||
.fontColor('#2C231D')
|
|
||||||
.margin({ bottom: 4 })
|
|
||||||
}
|
|
||||||
.alignItems(VerticalAlign.Bottom)
|
|
||||||
|
|
||||||
Text(this.overdue > 0 ? `逾期 ${this.overdue} 件` : `待办 ${this.todo} 件`)
|
|
||||||
.fontSize(10)
|
|
||||||
.fontColor(this.overdue > 0 ? '#C0533F' : '#A8988A')
|
|
||||||
.maxLines(1)
|
|
||||||
}
|
|
||||||
.width('38%')
|
|
||||||
.height('100%')
|
|
||||||
.alignItems(HorizontalAlign.Start)
|
|
||||||
|
|
||||||
// 分隔线(视觉上把"账"和"事"分开)
|
|
||||||
Divider()
|
|
||||||
.vertical(true)
|
|
||||||
.strokeWidth(0.5)
|
|
||||||
.color('#EFE6D8')
|
|
||||||
.height('86%')
|
|
||||||
|
|
||||||
// ---- 右栏:今天的事 ----
|
|
||||||
Column({ space: 0 }) {
|
|
||||||
Text('今天')
|
|
||||||
.fontSize(10)
|
|
||||||
.fontColor('#A8988A')
|
|
||||||
.width('100%')
|
|
||||||
.margin({ bottom: 5 })
|
|
||||||
|
|
||||||
if (this.hint !== '') {
|
|
||||||
Text(this.hint)
|
|
||||||
.fontSize(11)
|
|
||||||
.fontColor('#C0533F')
|
|
||||||
.maxLines(2)
|
|
||||||
.width('100%')
|
|
||||||
} else if (this.top3().length === 0) {
|
|
||||||
Text('今天没有安排')
|
|
||||||
.fontSize(11)
|
|
||||||
.fontColor('#A8988A')
|
|
||||||
.width('100%')
|
|
||||||
} else {
|
|
||||||
ForEach(this.top3(), (e: FbCardEvent) => {
|
|
||||||
Row({ space: 6 }) {
|
|
||||||
Column()
|
|
||||||
.width(3)
|
|
||||||
.height(13)
|
|
||||||
.borderRadius(2)
|
|
||||||
.backgroundColor(e.color)
|
|
||||||
.flexShrink(0)
|
|
||||||
Text(e.time)
|
|
||||||
.fontSize(10)
|
|
||||||
.fontColor('#7E6B5A')
|
|
||||||
.width(34)
|
|
||||||
.flexShrink(0)
|
|
||||||
Text(e.title)
|
|
||||||
.fontSize(11)
|
|
||||||
.fontColor('#2C231D')
|
|
||||||
.maxLines(1)
|
|
||||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
|
||||||
.layoutWeight(1)
|
|
||||||
}
|
|
||||||
.width('100%')
|
|
||||||
.height(20)
|
|
||||||
.alignItems(VerticalAlign.Center)
|
|
||||||
}, (e: FbCardEvent) => e.key)
|
|
||||||
}
|
|
||||||
Blank()
|
|
||||||
}
|
|
||||||
.layoutWeight(1)
|
|
||||||
.height('100%')
|
|
||||||
.alignItems(HorizontalAlign.Start)
|
|
||||||
}
|
}
|
||||||
.width('100%')
|
.width(26)
|
||||||
.height('100%')
|
.height(26)
|
||||||
.padding({ left: 14, right: 14, top: 12, bottom: 12 })
|
.borderRadius(13)
|
||||||
.backgroundColor('#FFFFFF')
|
.padding(0)
|
||||||
.borderRadius(16)
|
.backgroundColor('#A84A28')
|
||||||
.alignItems(VerticalAlign.Top)
|
|
||||||
.onClick((): void => {
|
.onClick((): void => {
|
||||||
postCardAction(this, {
|
postCardAction(this, {
|
||||||
action: 'router',
|
action: 'router',
|
||||||
abilityName: 'EntryAbility',
|
abilityName: 'EntryAbility',
|
||||||
params: { fbModule: 'calendar' }
|
params: { fbModule: 'calendar', widgetAction: 'add', openDate: `${this.selectedDate}` }
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
private openApp(): void {
|
||||||
|
postCardAction(this, {
|
||||||
|
action: 'router',
|
||||||
|
abilityName: 'EntryAbility',
|
||||||
|
params: { fbModule: 'calendar', openDate: `${this.selectedDate}` }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 右半单条:左色条 + 起止时间 + 标题 */
|
||||||
|
@Builder
|
||||||
|
upcomingRow(item: FbCardUp) {
|
||||||
|
Row({ space: 8 }) {
|
||||||
|
Column()
|
||||||
|
.width(3)
|
||||||
|
.height(38)
|
||||||
|
.borderRadius(2)
|
||||||
|
.backgroundColor(item.isNow ? '#C0533F' : item.color)
|
||||||
|
Column({ space: 2 }) {
|
||||||
|
Text(item.time)
|
||||||
|
.fontSize(10)
|
||||||
|
.fontWeight(FontWeight.Medium)
|
||||||
|
.fontColor(item.isNow ? '#C0533F' : '#5A4A3D')
|
||||||
|
.maxLines(1)
|
||||||
|
if (item.endTime !== '' && item.endTime !== '全天') {
|
||||||
|
Text(item.endTime)
|
||||||
|
.fontSize(9)
|
||||||
|
.fontColor('#A8988A')
|
||||||
|
.maxLines(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.width(36)
|
||||||
|
.alignItems(HorizontalAlign.Start)
|
||||||
|
Text(item.title)
|
||||||
|
.fontSize(13)
|
||||||
|
.fontColor('#2C231D')
|
||||||
|
.maxLines(2)
|
||||||
|
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||||
|
.layoutWeight(1)
|
||||||
|
}
|
||||||
|
.alignItems(VerticalAlign.Center)
|
||||||
|
.width('100%')
|
||||||
|
.padding({ left: 8, right: 8, top: 5, bottom: 5 })
|
||||||
|
.borderRadius(8)
|
||||||
|
.backgroundColor(item.isNow ? '#FBEEE9' : '#F7F2EA')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 右半空态:当天没有日程 / 只剩全天 */
|
||||||
|
@Builder
|
||||||
|
emptyHint() {
|
||||||
|
Column() {
|
||||||
|
Text(this.hint !== '' ? this.hint : (this.dayCount === 0 ? '今日暂无日程' : '只有全天日程'))
|
||||||
|
.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 }) {
|
||||||
|
Column({ space: 2 }) {
|
||||||
|
Text(this.dateMd === '' ? this.dateText : this.dateMd)
|
||||||
|
.fontSize(15)
|
||||||
|
.fontWeight(FontWeight.Bold)
|
||||||
|
.fontColor('#2C231D')
|
||||||
|
.maxLines(1)
|
||||||
|
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||||
|
Text(this.subLine())
|
||||||
|
.fontSize(10)
|
||||||
|
.fontColor('#A8988A')
|
||||||
|
.maxLines(1)
|
||||||
|
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||||
|
}
|
||||||
|
.layoutWeight(1)
|
||||||
|
.alignItems(HorizontalAlign.Start)
|
||||||
|
|
||||||
|
this.addButton()
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
.strokeWidth(0.5)
|
||||||
|
.color('#EFE6D8')
|
||||||
|
|
||||||
|
Row({ space: 6 }) {
|
||||||
|
Text(`${this.dayCount}`)
|
||||||
|
.fontSize(38)
|
||||||
|
.fontWeight(FontWeight.Bold)
|
||||||
|
.fontColor('#A84A28')
|
||||||
|
Column({ space: 2 }) {
|
||||||
|
Text('今日日程')
|
||||||
|
.fontSize(11)
|
||||||
|
.fontColor('#2C231D')
|
||||||
|
.maxLines(1)
|
||||||
|
Text(this.dayCount === 0 ? '点击添加' : '点击查看')
|
||||||
|
.fontSize(9)
|
||||||
|
.fontColor('#A8988A')
|
||||||
|
.maxLines(1)
|
||||||
|
}
|
||||||
|
.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.upcoming().length === 0) {
|
||||||
|
this.emptyHint()
|
||||||
|
} else {
|
||||||
|
Column({ space: 6 }) {
|
||||||
|
ForEach(this.upcoming(), (item: FbCardUp, idx: number) => {
|
||||||
|
this.upcomingRow(item)
|
||||||
|
}, (item: FbCardUp, idx: number) => `${idx}_${item.time}_${item.title}`)
|
||||||
|
}
|
||||||
|
.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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,235 +1,525 @@
|
|||||||
/**
|
/**
|
||||||
* 家书 FamilyBook · 4×4 服务卡片(今日全景)
|
* 家书 FamilyBook · 4×4 服务卡片(日程 · 当天时间轴 / 列表)
|
||||||
*
|
*
|
||||||
* 用户 2026-10-20:「做几个和 syncCalendar 差不多的卡片。」
|
* 用户 2026-10-20:「跟日程有关的桌面服务卡片,你就完全按照 synccalendar 来做就行。」
|
||||||
|
* ⇒ 照 `SyncCalendar/entry/src/main/ets/pages/widget/Widget4x4.ets`:
|
||||||
|
* · 顶栏:`‹ 10月20日 / 九月初一 › [今|+]` —— **翻页按钮**走
|
||||||
|
* `postCardAction({action:'message', params:{pageAction:'prev'}})`,
|
||||||
|
* 由 `FbFormAbility.onFormEvent` 改偏移、重新取数再推回来;
|
||||||
|
* · 主体:**时间轴**(8:00~18:00,越界的日程自动扩)或**列表**,
|
||||||
|
* 跟应用内「日程设置 → 日程显示方式」联动;
|
||||||
|
* · 今天才画当前时间红线;翻到别的日期不画(否则"昨天的 15:00 正在发生"很荒谬)。
|
||||||
*
|
*
|
||||||
* 这块是"不用打开应用就能看完"的那一块:
|
* ⚠️ 卡片不支持 `Scroll`,但支持 `List`:把"整日时间轴"当成**一个很高的 ListItem**,
|
||||||
* 抬头(日期/农历)→ 四个数字(日程/在做/待办/逾期)→ 今日日程 → 最近纪念日。
|
* 超出卡片窗口的部分靠上下滑动看(与 SyncCalendar 同一个做法)。
|
||||||
* 四块内容的信息密度正好对上 4×4;再塞(任务详情、往年的今天)就会被裁。
|
|
||||||
*
|
|
||||||
* ⚠️ 右上角那个 ↻ 是"手动刷新":
|
|
||||||
* 动态卡片**不能自己发起网络请求**,只能 `postCardAction({action:'message'})`
|
|
||||||
* 把消息发给 `FbFormAbility.onFormEvent`,由那边重新取数再推回来。
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { FbCardDay, FbCardEvent } from '../../data/FbCardData';
|
import { FbCardBlock, FbCardTlBlock, FbCardTlLine, fbCardTlLines } from '../../data/FbCardData';
|
||||||
|
|
||||||
|
/** 1 小时 = 多少 vp(时间轴的纵向刻度) */
|
||||||
|
const HOUR_VP: number = 36;
|
||||||
|
/** 小时刻度列宽 */
|
||||||
|
const GUTTER_VP: number = 22;
|
||||||
|
/** 色块最短高度(太矮就点不到、字也画不下) */
|
||||||
|
const MIN_BLOCK_VP: number = 14;
|
||||||
|
|
||||||
let storage4x4 = new LocalStorage();
|
let storage4x4 = new LocalStorage();
|
||||||
|
|
||||||
|
/** 列表模式的一行(与 `listJson` 同结构) */
|
||||||
|
class FbCardListRow {
|
||||||
|
key: string = '';
|
||||||
|
time: string = '';
|
||||||
|
endTime: string = '';
|
||||||
|
title: string = '';
|
||||||
|
color: string = '#A84A28';
|
||||||
|
calName: string = '';
|
||||||
|
isNow: boolean = false;
|
||||||
|
}
|
||||||
|
|
||||||
@Entry(storage4x4)
|
@Entry(storage4x4)
|
||||||
@Component
|
@Component
|
||||||
struct FbCard4x4 {
|
struct FbCard4x4 {
|
||||||
@LocalStorageProp('state') state: string = 'loading';
|
@LocalStorageProp('state') state: string = 'loading';
|
||||||
@LocalStorageProp('todayLabel') todayLabel: string = '';
|
|
||||||
@LocalStorageProp('weekday') weekday: string = '';
|
|
||||||
@LocalStorageProp('lunarShort') lunarShort: string = '';
|
|
||||||
@LocalStorageProp('marks') marks: string = '';
|
|
||||||
@LocalStorageProp('eventCount') eventCount: number = 0;
|
|
||||||
@LocalStorageProp('doing') doing: number = 0;
|
|
||||||
@LocalStorageProp('todo') todo: number = 0;
|
|
||||||
@LocalStorageProp('overdue') overdue: number = 0;
|
|
||||||
@LocalStorageProp('eventsJson') eventsJson: string = '[]';
|
|
||||||
@LocalStorageProp('daysJson') daysJson: string = '[]';
|
|
||||||
@LocalStorageProp('hint') hint: string = '';
|
@LocalStorageProp('hint') hint: string = '';
|
||||||
@LocalStorageProp('updatedAt') updatedAt: string = '';
|
@LocalStorageProp('dateMd') dateMd: string = '';
|
||||||
|
@LocalStorageProp('weekday') weekday: string = '';
|
||||||
|
@LocalStorageProp('lunarText') lunarText: string = '';
|
||||||
|
@LocalStorageProp('isToday') isToday: boolean = true;
|
||||||
|
@LocalStorageProp('selectedDate') selectedDate: number = 0;
|
||||||
|
@LocalStorageProp('dayCount') dayCount: number = 0;
|
||||||
|
@LocalStorageProp('listJson') listJson: string = '[]';
|
||||||
|
@LocalStorageProp('allDayJson') allDayJson: string = '[]';
|
||||||
|
@LocalStorageProp('timelineJson') timelineJson: string = '[]';
|
||||||
|
@LocalStorageProp('displayStyle') displayStyle: string = 'timeline';
|
||||||
|
@LocalStorageProp('viewStartHour') viewStartHour: number = 8;
|
||||||
|
@LocalStorageProp('viewEndHour') viewEndHour: number = 18;
|
||||||
|
@LocalStorageProp('nowRatio') nowRatio: number = -1;
|
||||||
|
|
||||||
private subLine(): string {
|
/** 小时刻度(含起点不含终点) */
|
||||||
const parts: string[] = [];
|
private hours(): number[] {
|
||||||
if (this.weekday !== '') {
|
const out: number[] = [];
|
||||||
parts.push(this.weekday);
|
for (let h = this.viewStartHour; h < this.viewEndHour; h++) {
|
||||||
}
|
out.push(h);
|
||||||
if (this.lunarShort !== '') {
|
|
||||||
parts.push(this.lunarShort);
|
|
||||||
}
|
|
||||||
return parts.join(' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 今日日程最多画 5 条(第 6 条开始一定被裁) */
|
|
||||||
private events(): FbCardEvent[] {
|
|
||||||
const all = JSON.parse(this.eventsJson) as FbCardEvent[];
|
|
||||||
const out: FbCardEvent[] = [];
|
|
||||||
for (let i = 0; i < all.length && i < 5; i++) {
|
|
||||||
out.push(all[i]);
|
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 最近纪念日最多 2 条(第三行会与"今日日程"抢高度) */
|
private hourText(h: number): string {
|
||||||
private days(): FbCardDay[] {
|
return h < 10 ? `0${h}` : `${h}`;
|
||||||
const all = JSON.parse(this.daysJson) as FbCardDay[];
|
|
||||||
const out: FbCardDay[] = [];
|
|
||||||
for (let i = 0; i < all.length && i < 2; i++) {
|
|
||||||
out.push(all[i]);
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 手动刷新(走 message,见文件头) */
|
/** 时间轴总高 */
|
||||||
private refresh(): void {
|
private contentH(): number {
|
||||||
|
const n: number = this.viewEndHour - this.viewStartHour;
|
||||||
|
return (n > 0 ? n : 1) * HOUR_VP;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 冲突组里的列(`ln.group` 是可空类型,ForEach 的闭包里 TS 收窄会失效
|
||||||
|
* ⇒ 统一用这个取值函数,避免到处写非空断言)。
|
||||||
|
*/
|
||||||
|
private lanesOf(ln: FbCardTlLine): FbCardTlBlock[][] {
|
||||||
|
const g = ln.group;
|
||||||
|
return g === null ? [] : g.lanes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private groupH(ln: FbCardTlLine): number {
|
||||||
|
const g = ln.group;
|
||||||
|
return g === null ? 0 : g.heightVp;
|
||||||
|
}
|
||||||
|
|
||||||
|
private blocks(): FbCardBlock[] {
|
||||||
|
try {
|
||||||
|
return JSON.parse(this.timelineJson) as FbCardBlock[];
|
||||||
|
} catch (err) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 把比例换算成"行"(空白 / 冲突组),视图直接 ForEach */
|
||||||
|
private lines(): FbCardTlLine[] {
|
||||||
|
return fbCardTlLines(this.blocks(), this.viewStartHour, HOUR_VP);
|
||||||
|
}
|
||||||
|
|
||||||
|
private rows(): FbCardListRow[] {
|
||||||
|
try {
|
||||||
|
return JSON.parse(this.listJson) as FbCardListRow[];
|
||||||
|
} catch (err) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private allDay(): FbCardListRow[] {
|
||||||
|
try {
|
||||||
|
return JSON.parse(this.allDayJson) as FbCardListRow[];
|
||||||
|
} catch (err) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 当前时间红线在时间轴上的偏移(vp);不画时返回 -1 */
|
||||||
|
private nowOffsetVp(): number {
|
||||||
|
if (!this.isToday || this.nowRatio < 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
const off: number = (this.nowRatio * 24 - this.viewStartHour) * HOUR_VP;
|
||||||
|
if (off < 0 || off > this.contentH()) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return off;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 打开应用并定位到卡片显示的那天 */
|
||||||
|
private openApp(): void {
|
||||||
postCardAction(this, {
|
postCardAction(this, {
|
||||||
action: 'message',
|
action: 'router',
|
||||||
params: { msg: 'refresh' }
|
abilityName: 'EntryAbility',
|
||||||
|
params: { fbModule: 'calendar', openDate: `${this.selectedDate}` }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 一个数字格(日程 / 在做 / 待办 / 逾期) */
|
/** 翻页:交给 FbFormAbility.onFormEvent 重新取数并推送 */
|
||||||
@Builder
|
private page(act: string): void {
|
||||||
statCell(label: string, value: number, color: string) {
|
postCardAction(this, {
|
||||||
Column({ space: 1 }) {
|
action: 'message',
|
||||||
Text(`${value}`)
|
params: { pageAction: act }
|
||||||
.fontSize(16)
|
});
|
||||||
.fontWeight(FontWeight.Bold)
|
|
||||||
.fontColor(color)
|
|
||||||
.maxLines(1)
|
|
||||||
Text(label)
|
|
||||||
.fontSize(9)
|
|
||||||
.fontColor('#A8988A')
|
|
||||||
.maxLines(1)
|
|
||||||
}
|
|
||||||
.layoutWeight(1)
|
|
||||||
.alignItems(HorizontalAlign.Center)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
build() {
|
@Builder
|
||||||
Column() {
|
private pageBtn(label: string, act: string) {
|
||||||
// ---- 抬头 ----
|
Button() {
|
||||||
Row({ space: 6 }) {
|
Text(label)
|
||||||
Column({ space: 1 }) {
|
.fontSize(14)
|
||||||
Text(this.todayLabel === '' ? '家书' : this.todayLabel)
|
.fontWeight(FontWeight.Medium)
|
||||||
.fontSize(17)
|
.fontColor('#5A4A3D')
|
||||||
.fontWeight(FontWeight.Medium)
|
}
|
||||||
.fontColor('#2C231D')
|
.width(22)
|
||||||
.maxLines(1)
|
.height(22)
|
||||||
Text(this.subLine() + (this.marks !== '' ? ' ' + this.marks : ''))
|
.borderRadius(11)
|
||||||
|
.padding(0)
|
||||||
|
.backgroundColor('#F3ECE2')
|
||||||
|
/* ⚠️ 无需阻止冒泡:onClick 非冒泡、子组件优先 → 点翻页不会连带打开应用 */
|
||||||
|
.onClick((): void => this.page(act))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 今天时是「+」(新建日程);翻到别的日期时是「今」(回到今天) */
|
||||||
|
@Builder
|
||||||
|
private sideBtn() {
|
||||||
|
if (this.isToday) {
|
||||||
|
Button() {
|
||||||
|
Text('+')
|
||||||
|
.fontSize(14)
|
||||||
|
.fontWeight(FontWeight.Medium)
|
||||||
|
.fontColor('#FFFFFF')
|
||||||
|
}
|
||||||
|
.width(22)
|
||||||
|
.height(22)
|
||||||
|
.borderRadius(11)
|
||||||
|
.padding(0)
|
||||||
|
.backgroundColor('#A84A28')
|
||||||
|
.onClick((): void => {
|
||||||
|
postCardAction(this, {
|
||||||
|
action: 'router',
|
||||||
|
abilityName: 'EntryAbility',
|
||||||
|
params: { fbModule: 'calendar', widgetAction: 'add', openDate: `${this.selectedDate}` }
|
||||||
|
});
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Button() {
|
||||||
|
Text('今')
|
||||||
|
.fontSize(11)
|
||||||
|
.fontWeight(FontWeight.Medium)
|
||||||
|
.fontColor('#FFFFFF')
|
||||||
|
}
|
||||||
|
.width(22)
|
||||||
|
.height(22)
|
||||||
|
.borderRadius(11)
|
||||||
|
.padding(0)
|
||||||
|
.backgroundColor('#A84A28')
|
||||||
|
.onClick((): void => this.page('today'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 全天日程色块(画在时间轴上方,随内容一起滚动) */
|
||||||
|
@Builder
|
||||||
|
private allDayChips() {
|
||||||
|
Column({ space: 2 }) {
|
||||||
|
ForEach(this.allDay(), (a: FbCardListRow, idx: number) => {
|
||||||
|
Row({ space: 4 }) {
|
||||||
|
Text('全天')
|
||||||
|
.fontSize(8)
|
||||||
|
.fontColor('#FFFFFF')
|
||||||
|
.backgroundColor('#26000000')
|
||||||
|
.borderRadius(5)
|
||||||
|
.padding({ left: 4, right: 4, top: 1, bottom: 1 })
|
||||||
|
Text(a.title)
|
||||||
.fontSize(10)
|
.fontSize(10)
|
||||||
.fontColor('#A8988A')
|
.fontWeight(FontWeight.Medium)
|
||||||
|
.fontColor('#FFFFFF')
|
||||||
.maxLines(1)
|
.maxLines(1)
|
||||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||||
|
.layoutWeight(1)
|
||||||
}
|
}
|
||||||
.alignItems(HorizontalAlign.Start)
|
.alignItems(VerticalAlign.Center)
|
||||||
.layoutWeight(1)
|
|
||||||
|
|
||||||
Text('↻')
|
|
||||||
.fontSize(14)
|
|
||||||
.fontColor('#7E6B5A')
|
|
||||||
.width(26)
|
|
||||||
.height(26)
|
|
||||||
.textAlign(TextAlign.Center)
|
|
||||||
.backgroundColor('#F5EFE4')
|
|
||||||
.borderRadius(13)
|
|
||||||
.onClick((): void => {
|
|
||||||
this.refresh();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
.width('100%')
|
|
||||||
|
|
||||||
// ---- 四个数字 ----
|
|
||||||
Row() {
|
|
||||||
this.statCell('日程', this.eventCount, '#A84A28')
|
|
||||||
this.statCell('在做', this.doing, '#2C231D')
|
|
||||||
this.statCell('待办', this.todo, '#2C231D')
|
|
||||||
this.statCell('逾期', this.overdue, this.overdue > 0 ? '#C0533F' : '#A8988A')
|
|
||||||
}
|
|
||||||
.width('100%')
|
|
||||||
.margin({ top: 8 })
|
|
||||||
|
|
||||||
Divider()
|
|
||||||
.strokeWidth(0.5)
|
|
||||||
.color('#EFE6D8')
|
|
||||||
.margin({ top: 8, bottom: 6 })
|
|
||||||
|
|
||||||
// ---- 今日日程 ----
|
|
||||||
Text('今日日程')
|
|
||||||
.fontSize(10)
|
|
||||||
.fontColor('#A8988A')
|
|
||||||
.width('100%')
|
.width('100%')
|
||||||
|
.height(18)
|
||||||
|
.padding({ left: 5, right: 5 })
|
||||||
|
.borderRadius(5)
|
||||||
|
.backgroundColor(a.color)
|
||||||
|
}, (a: FbCardListRow, idx: number) => `ad_${idx}_${a.key}`)
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.padding({ bottom: 3 })
|
||||||
|
}
|
||||||
|
|
||||||
Column({ space: 0 }) {
|
/** 第 1 层:整点网格 */
|
||||||
if (this.hint !== '') {
|
@Builder
|
||||||
Text(this.hint)
|
private gridLayer() {
|
||||||
.fontSize(11)
|
Column() {
|
||||||
.fontColor('#C0533F')
|
ForEach(this.hours(), (h: number) => {
|
||||||
.maxLines(2)
|
Column()
|
||||||
.width('100%')
|
.width('100%')
|
||||||
} else if (this.events().length === 0) {
|
.height(HOUR_VP)
|
||||||
Text('今天没有安排')
|
.border({ width: { bottom: 0.5 }, color: { bottom: '#14000000' } })
|
||||||
.fontSize(11)
|
.hitTestBehavior(HitTestMode.None)
|
||||||
.fontColor('#A8988A')
|
}, (h: number) => `gd${h}`)
|
||||||
.width('100%')
|
}
|
||||||
|
.width('100%')
|
||||||
|
.height('100%')
|
||||||
|
.hitTestBehavior(HitTestMode.None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 第 2 层:日程色块(空白行 + 冲突组按列并排) */
|
||||||
|
@Builder
|
||||||
|
private eventLayer() {
|
||||||
|
Column() {
|
||||||
|
ForEach(this.lines(), (ln: FbCardTlLine) => {
|
||||||
|
if (ln.group === null) {
|
||||||
|
Blank().height(ln.blankVp)
|
||||||
} else {
|
} else {
|
||||||
ForEach(this.events(), (e: FbCardEvent) => {
|
Row() {
|
||||||
Row({ space: 6 }) {
|
ForEach(this.lanesOf(ln), (lane: FbCardTlBlock[], li: number) => {
|
||||||
Column()
|
Column() {
|
||||||
.width(3)
|
ForEach(lane, (b: FbCardTlBlock, bi: number) => {
|
||||||
.height(13)
|
if (b.padTopVp > 0) {
|
||||||
.borderRadius(2)
|
Blank().height(b.padTopVp)
|
||||||
.backgroundColor(e.color)
|
}
|
||||||
.flexShrink(0)
|
Column({ space: 0 }) {
|
||||||
Text(e.time)
|
Text(b.title)
|
||||||
.fontSize(10)
|
.fontSize(9)
|
||||||
.fontColor('#7E6B5A')
|
.fontWeight(FontWeight.Medium)
|
||||||
.width(34)
|
.fontColor('#FFFFFF')
|
||||||
.flexShrink(0)
|
.maxLines(1)
|
||||||
Text(e.title)
|
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||||
.fontSize(11)
|
.width('100%')
|
||||||
.fontColor('#2C231D')
|
if (b.heightVp >= 30) {
|
||||||
.maxLines(1)
|
Text(b.timeText)
|
||||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
.fontSize(8)
|
||||||
.layoutWeight(1)
|
.fontColor('#E6FFFFFF')
|
||||||
}
|
.maxLines(1)
|
||||||
.width('100%')
|
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||||
.height(21)
|
.width('100%')
|
||||||
.alignItems(VerticalAlign.Center)
|
}
|
||||||
}, (e: FbCardEvent) => e.key)
|
}
|
||||||
}
|
.alignItems(HorizontalAlign.Start)
|
||||||
}
|
.padding({ left: 4, right: 2, top: 1, bottom: 1 })
|
||||||
.width('100%')
|
.borderRadius(4)
|
||||||
.margin({ top: 3 })
|
.backgroundColor(b.color)
|
||||||
.layoutWeight(1)
|
.opacity(b.isNow ? 1 : 0.92)
|
||||||
.alignItems(HorizontalAlign.Start)
|
.clip(true)
|
||||||
|
.width('100%')
|
||||||
// ---- 最近纪念日 ----
|
.height(b.heightVp < MIN_BLOCK_VP ? MIN_BLOCK_VP : b.heightVp)
|
||||||
if (this.days().length > 0) {
|
}, (b: FbCardTlBlock, bi: number) => `b_${b.key}_${bi}`)
|
||||||
Divider()
|
}
|
||||||
.strokeWidth(0.5)
|
|
||||||
.color('#EFE6D8')
|
|
||||||
.margin({ top: 4, bottom: 5 })
|
|
||||||
|
|
||||||
ForEach(this.days(), (d: FbCardDay) => {
|
|
||||||
Row({ space: 6 }) {
|
|
||||||
Text(d.days === 0 ? '今天' : `${d.days} 天`)
|
|
||||||
.fontSize(10)
|
|
||||||
.fontColor(d.soon ? '#A84A28' : '#A8988A')
|
|
||||||
.width(38)
|
|
||||||
.flexShrink(0)
|
|
||||||
Text(d.title)
|
|
||||||
.fontSize(11)
|
|
||||||
.fontColor('#2C231D')
|
|
||||||
.maxLines(1)
|
|
||||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
|
||||||
.layoutWeight(1)
|
.layoutWeight(1)
|
||||||
|
.height('100%')
|
||||||
|
.padding({ right: 2 })
|
||||||
|
.clip(true)
|
||||||
|
}, (lane: FbCardTlBlock[], li: number) => `ln_${ln.key}_${li}`)
|
||||||
}
|
}
|
||||||
.width('100%')
|
.width('100%')
|
||||||
.height(18)
|
.height(this.groupH(ln))
|
||||||
.alignItems(VerticalAlign.Center)
|
.alignItems(VerticalAlign.Top)
|
||||||
}, (d: FbCardDay) => d.key)
|
}
|
||||||
|
}, (ln: FbCardTlLine) => ln.key)
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.height('100%')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 第 3 层:当前时间红线(只有今天画) */
|
||||||
|
@Builder
|
||||||
|
private nowLayer() {
|
||||||
|
Column() {
|
||||||
|
if (this.nowOffsetVp() >= 0) {
|
||||||
|
Blank().height(this.nowOffsetVp()).hitTestBehavior(HitTestMode.None)
|
||||||
|
Row() {
|
||||||
|
Column()
|
||||||
|
.width(4)
|
||||||
|
.height(4)
|
||||||
|
.borderRadius(2)
|
||||||
|
.backgroundColor('#C0533F')
|
||||||
|
Column()
|
||||||
|
.height(2)
|
||||||
|
.layoutWeight(1)
|
||||||
|
.borderRadius(1)
|
||||||
|
.backgroundColor('#C0533F')
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.hitTestBehavior(HitTestMode.None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.width('100%')
|
.width('100%')
|
||||||
.height('100%')
|
.height('100%')
|
||||||
.padding({ left: 14, right: 14, top: 12, bottom: 10 })
|
.hitTestBehavior(HitTestMode.None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 时间轴主体 */
|
||||||
|
@Builder
|
||||||
|
private timelineBody() {
|
||||||
|
List() {
|
||||||
|
ListItem() {
|
||||||
|
Column() {
|
||||||
|
if (this.allDay().length > 0) {
|
||||||
|
this.allDayChips()
|
||||||
|
}
|
||||||
|
Row() {
|
||||||
|
Column() {
|
||||||
|
ForEach(this.hours(), (h: number) => {
|
||||||
|
Text(this.hourText(h))
|
||||||
|
.fontSize(9)
|
||||||
|
.fontColor('#A8988A')
|
||||||
|
.width(GUTTER_VP)
|
||||||
|
.height(HOUR_VP)
|
||||||
|
.textAlign(TextAlign.End)
|
||||||
|
.padding({ right: 3 })
|
||||||
|
.border({ width: { bottom: 0.5 }, color: { bottom: '#14000000' } })
|
||||||
|
}, (h: number) => `h${h}`)
|
||||||
|
}
|
||||||
|
.width(GUTTER_VP)
|
||||||
|
.height(this.contentH())
|
||||||
|
|
||||||
|
Stack() {
|
||||||
|
this.gridLayer()
|
||||||
|
this.eventLayer()
|
||||||
|
this.nowLayer()
|
||||||
|
}
|
||||||
|
.layoutWeight(1)
|
||||||
|
.height(this.contentH())
|
||||||
|
.alignContent(Alignment.TopStart)
|
||||||
|
.border({ width: { left: 0.5 }, color: { left: '#14000000' } })
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.height(this.contentH())
|
||||||
|
.alignItems(VerticalAlign.Top)
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
}
|
||||||
|
.layoutWeight(1)
|
||||||
|
.width('100%')
|
||||||
|
.scrollBar(BarState.Auto)
|
||||||
|
.cachedCount(4)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表模式的一行:左色条 + 起止时间 + 标题 [+ 进行中] + 日历本名 */
|
||||||
|
@Builder
|
||||||
|
private listRow(item: FbCardListRow) {
|
||||||
|
Row({ space: 8 }) {
|
||||||
|
Column()
|
||||||
|
.width(3)
|
||||||
|
.height(item.time === '全天' ? 16 : 36)
|
||||||
|
.borderRadius(2)
|
||||||
|
.backgroundColor(item.isNow ? '#C0533F' : item.color)
|
||||||
|
if (item.time === '全天') {
|
||||||
|
Text(item.title)
|
||||||
|
.fontSize(12)
|
||||||
|
.fontColor('#2C231D')
|
||||||
|
.maxLines(1)
|
||||||
|
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||||
|
.layoutWeight(1)
|
||||||
|
Text('全天')
|
||||||
|
.fontSize(9)
|
||||||
|
.fontColor('#FFFFFF')
|
||||||
|
.backgroundColor(item.color)
|
||||||
|
.borderRadius(6)
|
||||||
|
.padding({ left: 5, right: 5, top: 1, bottom: 1 })
|
||||||
|
} else {
|
||||||
|
Column({ space: 1 }) {
|
||||||
|
Text(item.time)
|
||||||
|
.fontSize(10)
|
||||||
|
.fontWeight(FontWeight.Medium)
|
||||||
|
.fontColor(item.isNow ? '#C0533F' : '#5A4A3D')
|
||||||
|
Text(item.endTime)
|
||||||
|
.fontSize(9)
|
||||||
|
.fontColor('#A8988A')
|
||||||
|
}
|
||||||
|
.width(34)
|
||||||
|
.alignItems(HorizontalAlign.Center)
|
||||||
|
Text(item.title)
|
||||||
|
.fontSize(12)
|
||||||
|
.fontColor(item.isNow ? '#C0533F' : '#2C231D')
|
||||||
|
.maxLines(2)
|
||||||
|
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||||
|
.layoutWeight(1)
|
||||||
|
if (item.isNow) {
|
||||||
|
Text('进行中')
|
||||||
|
.fontSize(9)
|
||||||
|
.fontColor('#C0533F')
|
||||||
|
.padding({ left: 4, right: 4, top: 1, bottom: 1 })
|
||||||
|
.borderRadius(4)
|
||||||
|
.backgroundColor('#FBEEE9')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (item.calName !== '') {
|
||||||
|
Text(item.calName)
|
||||||
|
.fontSize(9)
|
||||||
|
.fontColor(item.color)
|
||||||
|
.maxLines(1)
|
||||||
|
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||||
|
.constraintSize({ maxWidth: '25%' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.alignItems(VerticalAlign.Center)
|
||||||
|
.width('100%')
|
||||||
|
.padding({ left: 8, right: 8, top: 5, bottom: 5 })
|
||||||
|
.borderRadius(8)
|
||||||
|
.backgroundColor(item.isNow ? '#FBEEE9' : '#F7F2EA')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表模式主体:空态 / 可滚动的行列表 */
|
||||||
|
@Builder
|
||||||
|
private listBody() {
|
||||||
|
if (this.rows().length === 0) {
|
||||||
|
Column({ space: 6 }) {
|
||||||
|
Text(this.hint !== '' ? this.hint : '暂无日程')
|
||||||
|
.fontSize(13)
|
||||||
|
.fontColor(this.hint !== '' ? '#C0533F' : '#A8988A')
|
||||||
|
.maxLines(3)
|
||||||
|
.textAlign(TextAlign.Center)
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.layoutWeight(1)
|
||||||
|
.justifyContent(FlexAlign.Center)
|
||||||
|
} else {
|
||||||
|
List({ space: 4 }) {
|
||||||
|
ForEach(this.rows(), (item: FbCardListRow) => {
|
||||||
|
ListItem() {
|
||||||
|
this.listRow(item)
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
}, (item: FbCardListRow) => item.key)
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.layoutWeight(1)
|
||||||
|
.scrollBar(BarState.Auto)
|
||||||
|
.cachedCount(8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
build() {
|
||||||
|
Column({ space: 6 }) {
|
||||||
|
// ---- 顶栏:翻页 + 日期 + 今/+ ----
|
||||||
|
Row({ space: 4 }) {
|
||||||
|
this.pageBtn('‹', 'prev')
|
||||||
|
Column({ space: 1 }) {
|
||||||
|
Text(this.dateMd)
|
||||||
|
.fontSize(12)
|
||||||
|
.fontWeight(FontWeight.Bold)
|
||||||
|
.fontColor(this.isToday ? '#A84A28' : '#2C231D')
|
||||||
|
.maxLines(1)
|
||||||
|
Text(this.lunarText === '' ? this.weekday : this.lunarText)
|
||||||
|
.fontSize(9)
|
||||||
|
.fontColor('#A8988A')
|
||||||
|
.maxLines(1)
|
||||||
|
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||||
|
}
|
||||||
|
.layoutWeight(1)
|
||||||
|
.alignItems(HorizontalAlign.Start)
|
||||||
|
this.pageBtn('›', 'next')
|
||||||
|
this.sideBtn()
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
.strokeWidth(0.5)
|
||||||
|
.color('#EFE6D8')
|
||||||
|
|
||||||
|
if (this.displayStyle === 'list') {
|
||||||
|
this.listBody()
|
||||||
|
} else {
|
||||||
|
this.timelineBody()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.height('100%')
|
||||||
|
.padding(12)
|
||||||
.backgroundColor('#FFFFFF')
|
.backgroundColor('#FFFFFF')
|
||||||
.borderRadius(16)
|
.borderRadius(16)
|
||||||
.alignItems(HorizontalAlign.Start)
|
/* ⭐ 整卡可点:空白处也能进应用并定位到当前显示日。
|
||||||
.onClick((): void => {
|
⚠️ onClick 是**非冒泡**且**子组件优先** → 内部的 ‹ › 今 + 不会被这里抢走 */
|
||||||
// 整卡点击 → 打开日程(卡片上"今日日程"是主角)
|
.onClick((): void => this.openApp())
|
||||||
postCardAction(this, {
|
|
||||||
action: 'router',
|
|
||||||
abilityName: 'EntryAbility',
|
|
||||||
params: { fbModule: 'calendar' }
|
|
||||||
});
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,522 @@
|
|||||||
|
/**
|
||||||
|
* 家书 FamilyBook · 6×4 服务卡片(日程 · 当天时间轴 / 列表,横向更宽)
|
||||||
|
*
|
||||||
|
* 用户 2026-10-20:「跟日程有关的桌面服务卡片,你就完全按照 synccalendar 来做就行。」
|
||||||
|
* ⇒ 与 `FbCard4x4` 同一套结构(`SyncCalendar` 的 `Widget6x4` 与 `Widget4x4`
|
||||||
|
* 也是同一个版面、只是更宽),差别只有两处:
|
||||||
|
* · 1 小时 = 40vp(宽卡纵向能多给一点);
|
||||||
|
* · 小时刻度列更宽(26vp),时间数字不挤。
|
||||||
|
*
|
||||||
|
* ⚠️ 与 4×4 **必须**用同一份数据(`FbCardData`):两张卡同时放桌面上时,
|
||||||
|
* 条数与时间轴位置必须一致 —— 谁都不许自己再算一遍。
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { FbCardBlock, FbCardTlBlock, FbCardTlLine, fbCardTlLines } from '../../data/FbCardData';
|
||||||
|
|
||||||
|
/** 1 小时 = 多少 vp(时间轴的纵向刻度) */
|
||||||
|
const HOUR_VP: number = 40;
|
||||||
|
/** 小时刻度列宽 */
|
||||||
|
const GUTTER_VP: number = 26;
|
||||||
|
/** 色块最短高度(太矮就点不到、字也画不下) */
|
||||||
|
const MIN_BLOCK_VP: number = 14;
|
||||||
|
|
||||||
|
let storage6x4 = new LocalStorage();
|
||||||
|
|
||||||
|
/** 列表模式的一行(与 `listJson` 同结构) */
|
||||||
|
class FbCardListRow {
|
||||||
|
key: string = '';
|
||||||
|
time: string = '';
|
||||||
|
endTime: string = '';
|
||||||
|
title: string = '';
|
||||||
|
color: string = '#A84A28';
|
||||||
|
calName: string = '';
|
||||||
|
isNow: boolean = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entry(storage6x4)
|
||||||
|
@Component
|
||||||
|
struct FbCard6x4 {
|
||||||
|
@LocalStorageProp('state') state: string = 'loading';
|
||||||
|
@LocalStorageProp('hint') hint: string = '';
|
||||||
|
@LocalStorageProp('dateMd') dateMd: string = '';
|
||||||
|
@LocalStorageProp('weekday') weekday: string = '';
|
||||||
|
@LocalStorageProp('lunarText') lunarText: string = '';
|
||||||
|
@LocalStorageProp('isToday') isToday: boolean = true;
|
||||||
|
@LocalStorageProp('selectedDate') selectedDate: number = 0;
|
||||||
|
@LocalStorageProp('dayCount') dayCount: number = 0;
|
||||||
|
@LocalStorageProp('listJson') listJson: string = '[]';
|
||||||
|
@LocalStorageProp('allDayJson') allDayJson: string = '[]';
|
||||||
|
@LocalStorageProp('timelineJson') timelineJson: string = '[]';
|
||||||
|
@LocalStorageProp('displayStyle') displayStyle: string = 'timeline';
|
||||||
|
@LocalStorageProp('viewStartHour') viewStartHour: number = 8;
|
||||||
|
@LocalStorageProp('viewEndHour') viewEndHour: number = 18;
|
||||||
|
@LocalStorageProp('nowRatio') nowRatio: number = -1;
|
||||||
|
|
||||||
|
/** 小时刻度(含起点不含终点) */
|
||||||
|
private hours(): number[] {
|
||||||
|
const out: number[] = [];
|
||||||
|
for (let h = this.viewStartHour; h < this.viewEndHour; h++) {
|
||||||
|
out.push(h);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private hourText(h: number): string {
|
||||||
|
return h < 10 ? `0${h}` : `${h}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 时间轴总高 */
|
||||||
|
private contentH(): number {
|
||||||
|
const n: number = this.viewEndHour - this.viewStartHour;
|
||||||
|
return (n > 0 ? n : 1) * HOUR_VP;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 冲突组里的列(`ln.group` 是可空类型,ForEach 的闭包里 TS 收窄会失效
|
||||||
|
* ⇒ 统一用这个取值函数,避免到处写非空断言)。
|
||||||
|
*/
|
||||||
|
private lanesOf(ln: FbCardTlLine): FbCardTlBlock[][] {
|
||||||
|
const g = ln.group;
|
||||||
|
return g === null ? [] : g.lanes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private groupH(ln: FbCardTlLine): number {
|
||||||
|
const g = ln.group;
|
||||||
|
return g === null ? 0 : g.heightVp;
|
||||||
|
}
|
||||||
|
|
||||||
|
private blocks(): FbCardBlock[] {
|
||||||
|
try {
|
||||||
|
return JSON.parse(this.timelineJson) as FbCardBlock[];
|
||||||
|
} catch (err) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 把比例换算成"行"(空白 / 冲突组),视图直接 ForEach */
|
||||||
|
private lines(): FbCardTlLine[] {
|
||||||
|
return fbCardTlLines(this.blocks(), this.viewStartHour, HOUR_VP);
|
||||||
|
}
|
||||||
|
|
||||||
|
private rows(): FbCardListRow[] {
|
||||||
|
try {
|
||||||
|
return JSON.parse(this.listJson) as FbCardListRow[];
|
||||||
|
} catch (err) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private allDay(): FbCardListRow[] {
|
||||||
|
try {
|
||||||
|
return JSON.parse(this.allDayJson) as FbCardListRow[];
|
||||||
|
} catch (err) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 当前时间红线在时间轴上的偏移(vp);不画时返回 -1 */
|
||||||
|
private nowOffsetVp(): number {
|
||||||
|
if (!this.isToday || this.nowRatio < 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
const off: number = (this.nowRatio * 24 - this.viewStartHour) * HOUR_VP;
|
||||||
|
if (off < 0 || off > this.contentH()) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return off;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 打开应用并定位到卡片显示的那天 */
|
||||||
|
private openApp(): void {
|
||||||
|
postCardAction(this, {
|
||||||
|
action: 'router',
|
||||||
|
abilityName: 'EntryAbility',
|
||||||
|
params: { fbModule: 'calendar', openDate: `${this.selectedDate}` }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 翻页:交给 FbFormAbility.onFormEvent 重新取数并推送 */
|
||||||
|
private page(act: string): void {
|
||||||
|
postCardAction(this, {
|
||||||
|
action: 'message',
|
||||||
|
params: { pageAction: act }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Builder
|
||||||
|
private pageBtn(label: string, act: string) {
|
||||||
|
Button() {
|
||||||
|
Text(label)
|
||||||
|
.fontSize(14)
|
||||||
|
.fontWeight(FontWeight.Medium)
|
||||||
|
.fontColor('#5A4A3D')
|
||||||
|
}
|
||||||
|
.width(22)
|
||||||
|
.height(22)
|
||||||
|
.borderRadius(11)
|
||||||
|
.padding(0)
|
||||||
|
.backgroundColor('#F3ECE2')
|
||||||
|
/* ⚠️ 无需阻止冒泡:onClick 非冒泡、子组件优先 → 点翻页不会连带打开应用 */
|
||||||
|
.onClick((): void => this.page(act))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 今天时是「+」(新建日程);翻到别的日期时是「今」(回到今天) */
|
||||||
|
@Builder
|
||||||
|
private sideBtn() {
|
||||||
|
if (this.isToday) {
|
||||||
|
Button() {
|
||||||
|
Text('+')
|
||||||
|
.fontSize(14)
|
||||||
|
.fontWeight(FontWeight.Medium)
|
||||||
|
.fontColor('#FFFFFF')
|
||||||
|
}
|
||||||
|
.width(22)
|
||||||
|
.height(22)
|
||||||
|
.borderRadius(11)
|
||||||
|
.padding(0)
|
||||||
|
.backgroundColor('#A84A28')
|
||||||
|
.onClick((): void => {
|
||||||
|
postCardAction(this, {
|
||||||
|
action: 'router',
|
||||||
|
abilityName: 'EntryAbility',
|
||||||
|
params: { fbModule: 'calendar', widgetAction: 'add', openDate: `${this.selectedDate}` }
|
||||||
|
});
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Button() {
|
||||||
|
Text('今')
|
||||||
|
.fontSize(11)
|
||||||
|
.fontWeight(FontWeight.Medium)
|
||||||
|
.fontColor('#FFFFFF')
|
||||||
|
}
|
||||||
|
.width(22)
|
||||||
|
.height(22)
|
||||||
|
.borderRadius(11)
|
||||||
|
.padding(0)
|
||||||
|
.backgroundColor('#A84A28')
|
||||||
|
.onClick((): void => this.page('today'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 全天日程色块(画在时间轴上方,随内容一起滚动) */
|
||||||
|
@Builder
|
||||||
|
private allDayChips() {
|
||||||
|
Column({ space: 2 }) {
|
||||||
|
ForEach(this.allDay(), (a: FbCardListRow, idx: number) => {
|
||||||
|
Row({ space: 4 }) {
|
||||||
|
Text('全天')
|
||||||
|
.fontSize(8)
|
||||||
|
.fontColor('#FFFFFF')
|
||||||
|
.backgroundColor('#26000000')
|
||||||
|
.borderRadius(5)
|
||||||
|
.padding({ left: 4, right: 4, top: 1, bottom: 1 })
|
||||||
|
Text(a.title)
|
||||||
|
.fontSize(10)
|
||||||
|
.fontWeight(FontWeight.Medium)
|
||||||
|
.fontColor('#FFFFFF')
|
||||||
|
.maxLines(1)
|
||||||
|
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||||
|
.layoutWeight(1)
|
||||||
|
}
|
||||||
|
.alignItems(VerticalAlign.Center)
|
||||||
|
.width('100%')
|
||||||
|
.height(18)
|
||||||
|
.padding({ left: 5, right: 5 })
|
||||||
|
.borderRadius(5)
|
||||||
|
.backgroundColor(a.color)
|
||||||
|
}, (a: FbCardListRow, idx: number) => `ad_${idx}_${a.key}`)
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.padding({ bottom: 3 })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 第 1 层:整点网格 */
|
||||||
|
@Builder
|
||||||
|
private gridLayer() {
|
||||||
|
Column() {
|
||||||
|
ForEach(this.hours(), (h: number) => {
|
||||||
|
Column()
|
||||||
|
.width('100%')
|
||||||
|
.height(HOUR_VP)
|
||||||
|
.border({ width: { bottom: 0.5 }, color: { bottom: '#14000000' } })
|
||||||
|
.hitTestBehavior(HitTestMode.None)
|
||||||
|
}, (h: number) => `gd${h}`)
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.height('100%')
|
||||||
|
.hitTestBehavior(HitTestMode.None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 第 2 层:日程色块(空白行 + 冲突组按列并排) */
|
||||||
|
@Builder
|
||||||
|
private eventLayer() {
|
||||||
|
Column() {
|
||||||
|
ForEach(this.lines(), (ln: FbCardTlLine) => {
|
||||||
|
if (ln.group === null) {
|
||||||
|
Blank().height(ln.blankVp)
|
||||||
|
} else {
|
||||||
|
Row() {
|
||||||
|
ForEach(this.lanesOf(ln), (lane: FbCardTlBlock[], li: number) => {
|
||||||
|
Column() {
|
||||||
|
ForEach(lane, (b: FbCardTlBlock, bi: number) => {
|
||||||
|
if (b.padTopVp > 0) {
|
||||||
|
Blank().height(b.padTopVp)
|
||||||
|
}
|
||||||
|
Column({ space: 0 }) {
|
||||||
|
Text(b.title)
|
||||||
|
.fontSize(9)
|
||||||
|
.fontWeight(FontWeight.Medium)
|
||||||
|
.fontColor('#FFFFFF')
|
||||||
|
.maxLines(1)
|
||||||
|
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||||
|
.width('100%')
|
||||||
|
if (b.heightVp >= 30) {
|
||||||
|
Text(b.timeText)
|
||||||
|
.fontSize(8)
|
||||||
|
.fontColor('#E6FFFFFF')
|
||||||
|
.maxLines(1)
|
||||||
|
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||||
|
.width('100%')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.alignItems(HorizontalAlign.Start)
|
||||||
|
.padding({ left: 4, right: 2, top: 1, bottom: 1 })
|
||||||
|
.borderRadius(4)
|
||||||
|
.backgroundColor(b.color)
|
||||||
|
.opacity(b.isNow ? 1 : 0.92)
|
||||||
|
.clip(true)
|
||||||
|
.width('100%')
|
||||||
|
.height(b.heightVp < MIN_BLOCK_VP ? MIN_BLOCK_VP : b.heightVp)
|
||||||
|
}, (b: FbCardTlBlock, bi: number) => `b_${b.key}_${bi}`)
|
||||||
|
}
|
||||||
|
.layoutWeight(1)
|
||||||
|
.height('100%')
|
||||||
|
.padding({ right: 2 })
|
||||||
|
.clip(true)
|
||||||
|
}, (lane: FbCardTlBlock[], li: number) => `ln_${ln.key}_${li}`)
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.height(this.groupH(ln))
|
||||||
|
.alignItems(VerticalAlign.Top)
|
||||||
|
}
|
||||||
|
}, (ln: FbCardTlLine) => ln.key)
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.height('100%')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 第 3 层:当前时间红线(只有今天画) */
|
||||||
|
@Builder
|
||||||
|
private nowLayer() {
|
||||||
|
Column() {
|
||||||
|
if (this.nowOffsetVp() >= 0) {
|
||||||
|
Blank().height(this.nowOffsetVp()).hitTestBehavior(HitTestMode.None)
|
||||||
|
Row() {
|
||||||
|
Column()
|
||||||
|
.width(4)
|
||||||
|
.height(4)
|
||||||
|
.borderRadius(2)
|
||||||
|
.backgroundColor('#C0533F')
|
||||||
|
Column()
|
||||||
|
.height(2)
|
||||||
|
.layoutWeight(1)
|
||||||
|
.borderRadius(1)
|
||||||
|
.backgroundColor('#C0533F')
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.hitTestBehavior(HitTestMode.None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.height('100%')
|
||||||
|
.hitTestBehavior(HitTestMode.None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 时间轴主体 */
|
||||||
|
@Builder
|
||||||
|
private timelineBody() {
|
||||||
|
List() {
|
||||||
|
ListItem() {
|
||||||
|
Column() {
|
||||||
|
if (this.allDay().length > 0) {
|
||||||
|
this.allDayChips()
|
||||||
|
}
|
||||||
|
Row() {
|
||||||
|
Column() {
|
||||||
|
ForEach(this.hours(), (h: number) => {
|
||||||
|
Text(this.hourText(h))
|
||||||
|
.fontSize(9)
|
||||||
|
.fontColor('#A8988A')
|
||||||
|
.width(GUTTER_VP)
|
||||||
|
.height(HOUR_VP)
|
||||||
|
.textAlign(TextAlign.End)
|
||||||
|
.padding({ right: 3 })
|
||||||
|
.border({ width: { bottom: 0.5 }, color: { bottom: '#14000000' } })
|
||||||
|
}, (h: number) => `h${h}`)
|
||||||
|
}
|
||||||
|
.width(GUTTER_VP)
|
||||||
|
.height(this.contentH())
|
||||||
|
|
||||||
|
Stack() {
|
||||||
|
this.gridLayer()
|
||||||
|
this.eventLayer()
|
||||||
|
this.nowLayer()
|
||||||
|
}
|
||||||
|
.layoutWeight(1)
|
||||||
|
.height(this.contentH())
|
||||||
|
.alignContent(Alignment.TopStart)
|
||||||
|
.border({ width: { left: 0.5 }, color: { left: '#14000000' } })
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.height(this.contentH())
|
||||||
|
.alignItems(VerticalAlign.Top)
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
}
|
||||||
|
.layoutWeight(1)
|
||||||
|
.width('100%')
|
||||||
|
.scrollBar(BarState.Auto)
|
||||||
|
.cachedCount(4)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表模式的一行:左色条 + 起止时间 + 标题 [+ 进行中] + 日历本名 */
|
||||||
|
@Builder
|
||||||
|
private listRow(item: FbCardListRow) {
|
||||||
|
Row({ space: 8 }) {
|
||||||
|
Column()
|
||||||
|
.width(3)
|
||||||
|
.height(item.time === '全天' ? 16 : 36)
|
||||||
|
.borderRadius(2)
|
||||||
|
.backgroundColor(item.isNow ? '#C0533F' : item.color)
|
||||||
|
if (item.time === '全天') {
|
||||||
|
Text(item.title)
|
||||||
|
.fontSize(12)
|
||||||
|
.fontColor('#2C231D')
|
||||||
|
.maxLines(1)
|
||||||
|
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||||
|
.layoutWeight(1)
|
||||||
|
Text('全天')
|
||||||
|
.fontSize(9)
|
||||||
|
.fontColor('#FFFFFF')
|
||||||
|
.backgroundColor(item.color)
|
||||||
|
.borderRadius(6)
|
||||||
|
.padding({ left: 5, right: 5, top: 1, bottom: 1 })
|
||||||
|
} else {
|
||||||
|
Column({ space: 1 }) {
|
||||||
|
Text(item.time)
|
||||||
|
.fontSize(10)
|
||||||
|
.fontWeight(FontWeight.Medium)
|
||||||
|
.fontColor(item.isNow ? '#C0533F' : '#5A4A3D')
|
||||||
|
Text(item.endTime)
|
||||||
|
.fontSize(9)
|
||||||
|
.fontColor('#A8988A')
|
||||||
|
}
|
||||||
|
.width(34)
|
||||||
|
.alignItems(HorizontalAlign.Center)
|
||||||
|
Text(item.title)
|
||||||
|
.fontSize(12)
|
||||||
|
.fontColor(item.isNow ? '#C0533F' : '#2C231D')
|
||||||
|
.maxLines(2)
|
||||||
|
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||||
|
.layoutWeight(1)
|
||||||
|
if (item.isNow) {
|
||||||
|
Text('进行中')
|
||||||
|
.fontSize(9)
|
||||||
|
.fontColor('#C0533F')
|
||||||
|
.padding({ left: 4, right: 4, top: 1, bottom: 1 })
|
||||||
|
.borderRadius(4)
|
||||||
|
.backgroundColor('#FBEEE9')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (item.calName !== '') {
|
||||||
|
Text(item.calName)
|
||||||
|
.fontSize(9)
|
||||||
|
.fontColor(item.color)
|
||||||
|
.maxLines(1)
|
||||||
|
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||||
|
.constraintSize({ maxWidth: '25%' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.alignItems(VerticalAlign.Center)
|
||||||
|
.width('100%')
|
||||||
|
.padding({ left: 8, right: 8, top: 5, bottom: 5 })
|
||||||
|
.borderRadius(8)
|
||||||
|
.backgroundColor(item.isNow ? '#FBEEE9' : '#F7F2EA')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表模式主体:空态 / 可滚动的行列表 */
|
||||||
|
@Builder
|
||||||
|
private listBody() {
|
||||||
|
if (this.rows().length === 0) {
|
||||||
|
Column({ space: 6 }) {
|
||||||
|
Text(this.hint !== '' ? this.hint : '暂无日程')
|
||||||
|
.fontSize(13)
|
||||||
|
.fontColor(this.hint !== '' ? '#C0533F' : '#A8988A')
|
||||||
|
.maxLines(3)
|
||||||
|
.textAlign(TextAlign.Center)
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.layoutWeight(1)
|
||||||
|
.justifyContent(FlexAlign.Center)
|
||||||
|
} else {
|
||||||
|
List({ space: 4 }) {
|
||||||
|
ForEach(this.rows(), (item: FbCardListRow) => {
|
||||||
|
ListItem() {
|
||||||
|
this.listRow(item)
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
}, (item: FbCardListRow) => item.key)
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.layoutWeight(1)
|
||||||
|
.scrollBar(BarState.Auto)
|
||||||
|
.cachedCount(8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
build() {
|
||||||
|
Column({ space: 6 }) {
|
||||||
|
// ---- 顶栏:翻页 + 日期 + 今/+ ----
|
||||||
|
Row({ space: 4 }) {
|
||||||
|
this.pageBtn('‹', 'prev')
|
||||||
|
Column({ space: 1 }) {
|
||||||
|
Text(this.dateMd)
|
||||||
|
.fontSize(12)
|
||||||
|
.fontWeight(FontWeight.Bold)
|
||||||
|
.fontColor(this.isToday ? '#A84A28' : '#2C231D')
|
||||||
|
.maxLines(1)
|
||||||
|
Text(this.lunarText === '' ? this.weekday : this.lunarText)
|
||||||
|
.fontSize(9)
|
||||||
|
.fontColor('#A8988A')
|
||||||
|
.maxLines(1)
|
||||||
|
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||||
|
}
|
||||||
|
.layoutWeight(1)
|
||||||
|
.alignItems(HorizontalAlign.Start)
|
||||||
|
this.pageBtn('›', 'next')
|
||||||
|
this.sideBtn()
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
.strokeWidth(0.5)
|
||||||
|
.color('#EFE6D8')
|
||||||
|
|
||||||
|
if (this.displayStyle === 'list') {
|
||||||
|
this.listBody()
|
||||||
|
} else {
|
||||||
|
this.timelineBody()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.height('100%')
|
||||||
|
.padding(12)
|
||||||
|
.backgroundColor('#FFFFFF')
|
||||||
|
.borderRadius(16)
|
||||||
|
/* ⭐ 整卡可点:空白处也能进应用并定位到当前显示日。
|
||||||
|
⚠️ onClick 是**非冒泡**且**子组件优先** → 内部的 ‹ › 今 + 不会被这里抢走 */
|
||||||
|
.onClick((): void => this.openApp())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "card_desc",
|
"name": "card_desc",
|
||||||
"value": "今天的日程与最近的纪念日"
|
"value": "家庭日程:今天有几件事、都是什么"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "card_2x2_name",
|
"name": "card_2x2_name",
|
||||||
@@ -30,7 +30,11 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "card_4x4_name",
|
"name": "card_4x4_name",
|
||||||
"value": "家书 · 今日全景"
|
"value": "家书 · 日程"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "card_6x4_name",
|
||||||
|
"value": "家书 · 日程(宽)"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,23 @@
|
|||||||
"updateDuration": 1,
|
"updateDuration": 1,
|
||||||
"defaultDimension": "4*4",
|
"defaultDimension": "4*4",
|
||||||
"supportDimensions": ["4*4"]
|
"supportDimensions": ["4*4"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "FbCard6x4",
|
||||||
|
"displayName": "$string:card_6x4_name",
|
||||||
|
"description": "$string:card_desc",
|
||||||
|
"src": "./ets/pages/card/FbCard6x4.ets",
|
||||||
|
"window": {
|
||||||
|
"designWidth": 720,
|
||||||
|
"autoDesignWidth": true
|
||||||
|
},
|
||||||
|
"colorMode": "light",
|
||||||
|
"uiSyntax": "arkts",
|
||||||
|
"isDefault": false,
|
||||||
|
"updateEnabled": true,
|
||||||
|
"updateDuration": 1,
|
||||||
|
"defaultDimension": "6*4",
|
||||||
|
"supportDimensions": ["6*4"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user