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:
@@ -24,12 +24,16 @@
|
||||
*/
|
||||
|
||||
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 { FbCalBook, FbCalEvent, isEditableEvent } from '../../model/Types';
|
||||
import { parseCalendar } from '../../data/FbParse';
|
||||
import { loadModule } from '../../data/FbModuleLoad';
|
||||
import { FbCalRepo, FbCalEventFull } from '../../data/FbCalRepo';
|
||||
import { FB_CAL_SETTINGS, FbCalView, FbCalStyle } from '../../data/FbCalSettings';
|
||||
import { FB_CARD_FORMS } from '../../data/FbCardForms';
|
||||
import { FB_TOKEN_STORE } from '../../data/FbTokenStore';
|
||||
import { FbEmptyBox, FbIconButton } from '../../components/Widgets';
|
||||
import { FbEventSheet } from '../../components/FbEventSheet';
|
||||
@@ -288,10 +292,45 @@ export struct FbCalendarPage {
|
||||
this.mode = FB_CAL_SETTINGS.getView();
|
||||
this.style = FB_CAL_SETTINGS.getStyle();
|
||||
this.pickedIso = this.todayIso();
|
||||
// 桌面日程卡片可能指定了"看哪一天 / 直接新建"(冷启动入口)
|
||||
this.applyCardJump();
|
||||
this.load();
|
||||
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.load();
|
||||
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`:
|
||||
* 日期(月日)+ 星期/农历 + 当天日程条数大数字;右上角 + 直接新建日程;
|
||||
* **整卡可点**(空白处也能进应用,并定位到卡片显示的那一天)。
|
||||
*
|
||||
* 这块卡只回答一句话:**今天有几件事、最要紧的那件是什么**。
|
||||
* 所以版面是「日期 → 大数字 → 一行提示」,不做列表(2×2 放列表只会挤成蚂蚁)。
|
||||
*
|
||||
* ⚠️ 卡片里**不做任何计算**(不排序、不筛选、不拼日期):
|
||||
* 全部由 `data/FbCardData.ets` 算好塞进 `LocalStorage`(原因见那边的文件头)。
|
||||
* 这里只负责画 —— 三种尺寸共用同一份数据,数字必须一致。
|
||||
* ⚠️ 动态卡片**不能自己发网络请求**:这里只画 `FbCardData` 推过来的字段,
|
||||
* 要重新取数只能 `postCardAction({action:'message'})` 交给 `FbFormAbility`。
|
||||
*/
|
||||
|
||||
import { FbCardDay, FbCardEvent } from '../../data/FbCardData';
|
||||
|
||||
let storage2x2 = new LocalStorage();
|
||||
|
||||
@Entry(storage2x2)
|
||||
@Component
|
||||
struct FbCard2x2 {
|
||||
@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('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 {
|
||||
const parts: string[] = [];
|
||||
if (this.weekday !== '') {
|
||||
parts.push(this.weekday);
|
||||
}
|
||||
if (this.lunarShort !== '') {
|
||||
parts.push(this.lunarShort);
|
||||
if (this.lunarText !== '') {
|
||||
parts.push(this.lunarText);
|
||||
}
|
||||
return parts.join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* 最下面那一行:优先"最近的纪念日倒计时",其次"今天第一条日程"。
|
||||
*
|
||||
* ⚠️ 不显示"共 N 条"这种废话 —— 大数字已经说了。
|
||||
*/
|
||||
private footLine(): string {
|
||||
if (this.hint !== '') {
|
||||
return this.hint;
|
||||
/** 右上角 +:拉起应用直接进「新建日程」 */
|
||||
@Builder
|
||||
addButton() {
|
||||
Button() {
|
||||
Text('+')
|
||||
.fontSize(20)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor('#FFFFFF')
|
||||
}
|
||||
const days = JSON.parse(this.daysJson) as FbCardDay[];
|
||||
if (days.length > 0) {
|
||||
const d: FbCardDay = days[0];
|
||||
return d.days === 0 ? `${d.title} 就是今天` : `${d.title} 还有 ${d.days} 天`;
|
||||
}
|
||||
const evs = JSON.parse(this.eventsJson) as FbCardEvent[];
|
||||
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)
|
||||
// 整卡点击 → 打开日程(这张卡的主角就是"今天有几件事")
|
||||
.width(30)
|
||||
.height(30)
|
||||
.borderRadius(15)
|
||||
.padding(0)
|
||||
.backgroundColor('#A84A28')
|
||||
/* ⚠️ 无需阻止冒泡:ArkUI 的 `onClick` 是**非冒泡**事件,父子同绑时**子组件优先**,
|
||||
故点 + 不会连带触发根容器的"打开日程"。 */
|
||||
.onClick((): void => {
|
||||
postCardAction(this, {
|
||||
action: 'router',
|
||||
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 只能给个数字,说不出"是什么事";这块卡就是补那一步。
|
||||
*
|
||||
* ⚠️ 与 2×2 / 4×4 **同一份数据**(`data/FbCardData.ets` 算一次,三卡共用),
|
||||
* 所以件数一定一致 —— 不要在卡片里自己算。
|
||||
* ⚠️ 与 2×2 / 4×4 / 6×4 **同一份数据**(`data/FbCardData.ets` 算一次,四卡共用),
|
||||
* 所以条数一定一致 —— 不要在卡片里自己算。
|
||||
*/
|
||||
|
||||
import { FbCardDay, FbCardEvent } from '../../data/FbCardData';
|
||||
|
||||
let storage2x4 = new LocalStorage();
|
||||
|
||||
/** 右半「接下来两条」(与 `listJson` 同结构,这里只用到这几个字段) */
|
||||
class FbCardUp {
|
||||
title: string = '';
|
||||
time: string = '';
|
||||
endTime: string = '';
|
||||
color: string = '#A84A28';
|
||||
isNow: boolean = false;
|
||||
}
|
||||
|
||||
@Entry(storage2x4)
|
||||
@Component
|
||||
struct FbCard2x4 {
|
||||
@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('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 {
|
||||
const parts: string[] = [];
|
||||
if (this.weekday !== '') {
|
||||
parts.push(this.weekday);
|
||||
}
|
||||
if (this.lunarShort !== '') {
|
||||
parts.push(this.lunarShort);
|
||||
if (this.lunarText !== '') {
|
||||
parts.push(this.lunarText);
|
||||
}
|
||||
return parts.join(' ');
|
||||
}
|
||||
|
||||
/** 右侧最多三条(再多 2×4 的高度放不下,会被裁掉半行) */
|
||||
private top3(): FbCardEvent[] {
|
||||
const all = JSON.parse(this.eventsJson) as FbCardEvent[];
|
||||
const out: FbCardEvent[] = [];
|
||||
for (let i = 0; i < all.length && i < 3; i++) {
|
||||
out.push(all[i]);
|
||||
/** 右半最多两行(数据侧已裁剪,这里再兜底截断) */
|
||||
private upcoming(): FbCardUp[] {
|
||||
try {
|
||||
const all: FbCardUp[] = JSON.parse(this.upcomingJson) as FbCardUp[];
|
||||
return all.slice(0, 2);
|
||||
} catch (err) {
|
||||
return [];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
build() {
|
||||
Row({ space: 10 }) {
|
||||
// ---- 左栏:今天的"账" ----
|
||||
Column({ space: 2 }) {
|
||||
Text(this.todayLabel === '' ? '家书' : this.todayLabel)
|
||||
.fontSize(15)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.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)
|
||||
@Builder
|
||||
addButton() {
|
||||
Button() {
|
||||
Text('+')
|
||||
.fontSize(18)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor('#FFFFFF')
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.padding({ left: 14, right: 14, top: 12, bottom: 12 })
|
||||
.backgroundColor('#FFFFFF')
|
||||
.borderRadius(16)
|
||||
.alignItems(VerticalAlign.Top)
|
||||
.width(26)
|
||||
.height(26)
|
||||
.borderRadius(13)
|
||||
.padding(0)
|
||||
.backgroundColor('#A84A28')
|
||||
.onClick((): void => {
|
||||
postCardAction(this, {
|
||||
action: 'router',
|
||||
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 正在发生"很荒谬)。
|
||||
*
|
||||
* 这块是"不用打开应用就能看完"的那一块:
|
||||
* 抬头(日期/农历)→ 四个数字(日程/在做/待办/逾期)→ 今日日程 → 最近纪念日。
|
||||
* 四块内容的信息密度正好对上 4×4;再塞(任务详情、往年的今天)就会被裁。
|
||||
*
|
||||
* ⚠️ 右上角那个 ↻ 是"手动刷新":
|
||||
* 动态卡片**不能自己发起网络请求**,只能 `postCardAction({action:'message'})`
|
||||
* 把消息发给 `FbFormAbility.onFormEvent`,由那边重新取数再推回来。
|
||||
* ⚠️ 卡片不支持 `Scroll`,但支持 `List`:把"整日时间轴"当成**一个很高的 ListItem**,
|
||||
* 超出卡片窗口的部分靠上下滑动看(与 SyncCalendar 同一个做法)。
|
||||
*/
|
||||
|
||||
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();
|
||||
|
||||
/** 列表模式的一行(与 `listJson` 同结构) */
|
||||
class FbCardListRow {
|
||||
key: string = '';
|
||||
time: string = '';
|
||||
endTime: string = '';
|
||||
title: string = '';
|
||||
color: string = '#A84A28';
|
||||
calName: string = '';
|
||||
isNow: boolean = false;
|
||||
}
|
||||
|
||||
@Entry(storage4x4)
|
||||
@Component
|
||||
struct FbCard4x4 {
|
||||
@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('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[] = [];
|
||||
if (this.weekday !== '') {
|
||||
parts.push(this.weekday);
|
||||
}
|
||||
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]);
|
||||
/** 小时刻度(含起点不含终点) */
|
||||
private hours(): number[] {
|
||||
const out: number[] = [];
|
||||
for (let h = this.viewStartHour; h < this.viewEndHour; h++) {
|
||||
out.push(h);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 最近纪念日最多 2 条(第三行会与"今日日程"抢高度) */
|
||||
private days(): FbCardDay[] {
|
||||
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;
|
||||
private hourText(h: number): string {
|
||||
return h < 10 ? `0${h}` : `${h}`;
|
||||
}
|
||||
|
||||
/** 手动刷新(走 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, {
|
||||
action: 'message',
|
||||
params: { msg: 'refresh' }
|
||||
action: 'router',
|
||||
abilityName: 'EntryAbility',
|
||||
params: { fbModule: 'calendar', openDate: `${this.selectedDate}` }
|
||||
});
|
||||
}
|
||||
|
||||
/** 一个数字格(日程 / 在做 / 待办 / 逾期) */
|
||||
@Builder
|
||||
statCell(label: string, value: number, color: string) {
|
||||
Column({ space: 1 }) {
|
||||
Text(`${value}`)
|
||||
.fontSize(16)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
.fontColor(color)
|
||||
.maxLines(1)
|
||||
Text(label)
|
||||
.fontSize(9)
|
||||
.fontColor('#A8988A')
|
||||
.maxLines(1)
|
||||
}
|
||||
.layoutWeight(1)
|
||||
.alignItems(HorizontalAlign.Center)
|
||||
/** 翻页:交给 FbFormAbility.onFormEvent 重新取数并推送 */
|
||||
private page(act: string): void {
|
||||
postCardAction(this, {
|
||||
action: 'message',
|
||||
params: { pageAction: act }
|
||||
});
|
||||
}
|
||||
|
||||
build() {
|
||||
Column() {
|
||||
// ---- 抬头 ----
|
||||
Row({ space: 6 }) {
|
||||
Column({ space: 1 }) {
|
||||
Text(this.todayLabel === '' ? '家书' : this.todayLabel)
|
||||
.fontSize(17)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor('#2C231D')
|
||||
.maxLines(1)
|
||||
Text(this.subLine() + (this.marks !== '' ? ' ' + this.marks : ''))
|
||||
@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)
|
||||
.fontColor('#A8988A')
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor('#FFFFFF')
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
.layoutWeight(1)
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.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')
|
||||
.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 })
|
||||
}
|
||||
|
||||
Column({ space: 0 }) {
|
||||
if (this.hint !== '') {
|
||||
Text(this.hint)
|
||||
.fontSize(11)
|
||||
.fontColor('#C0533F')
|
||||
.maxLines(2)
|
||||
.width('100%')
|
||||
} else if (this.events().length === 0) {
|
||||
Text('今天没有安排')
|
||||
.fontSize(11)
|
||||
.fontColor('#A8988A')
|
||||
.width('100%')
|
||||
/** 第 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 {
|
||||
ForEach(this.events(), (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(21)
|
||||
.alignItems(VerticalAlign.Center)
|
||||
}, (e: FbCardEvent) => e.key)
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.margin({ top: 3 })
|
||||
.layoutWeight(1)
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
|
||||
// ---- 最近纪念日 ----
|
||||
if (this.days().length > 0) {
|
||||
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 })
|
||||
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(18)
|
||||
.alignItems(VerticalAlign.Center)
|
||||
}, (d: FbCardDay) => d.key)
|
||||
.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%')
|
||||
.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')
|
||||
.borderRadius(16)
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.onClick((): void => {
|
||||
// 整卡点击 → 打开日程(卡片上"今日日程"是主角)
|
||||
postCardAction(this, {
|
||||
action: 'router',
|
||||
abilityName: 'EntryAbility',
|
||||
params: { fbModule: 'calendar' }
|
||||
});
|
||||
})
|
||||
/* ⭐ 整卡可点:空白处也能进应用并定位到当前显示日。
|
||||
⚠️ onClick 是**非冒泡**且**子组件优先** → 内部的 ‹ › 今 + 不会被这里抢走 */
|
||||
.onClick((): void => this.openApp())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user