2026-10-11 22:07:48 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 家书 FamilyBook · 日程模块(鸿蒙端)
|
|
|
|
|
|
*
|
2026-10-18 03:35:39 +08:00
|
|
|
|
* 对应 Web 端 `templates/calendar/index.php`。
|
2026-10-11 22:07:48 +08:00
|
|
|
|
*
|
2026-10-18 03:35:39 +08:00
|
|
|
|
* 🔴 v52(用户 2026-10-15):「我们家书的日程功能模块,包括它的界面设计和功能,
|
|
|
|
|
|
* 跟 [SyncCalendar] 做得一样,就差不多。在新建的那个地方,右侧那加一个设置按钮,
|
|
|
|
|
|
* 点进去之后实际上就是**对这个日程的设置**。然后也是一样的,**有几种不同的视图方式**,
|
|
|
|
|
|
* 然后包括**下面日程的显示**啊什么之类的,就按照那个 SyncCalendar 这个模式去做就行了。」
|
2026-10-11 22:07:48 +08:00
|
|
|
|
*
|
2026-10-18 03:35:39 +08:00
|
|
|
|
* ⇒ 照 SyncCalendar 的骨架重做(`D:\Yongquan\code\SyncCalendar`):
|
|
|
|
|
|
* · **胶囊分段切视图**:月 / 周 / 列表(`FB_CAL_SETTINGS` 记住默认视图)
|
|
|
|
|
|
* · **月视图**:月历格子(日号 + 农历/选中/今天高亮)+ 当日日程
|
|
|
|
|
|
* · **周视图**:7 列日期条(周一→周日)+ 选中日日程
|
|
|
|
|
|
* · **列表视图**:从今天往后、按天分组
|
|
|
|
|
|
* · **右下角悬浮 +** 新建日程
|
|
|
|
|
|
* · **右上角设置齿轮** → 日程设置(默认视图 / 显示方式 / 日历本)
|
|
|
|
|
|
* · 点日程 → **详情浮层** → 「编辑日程」→ 编辑浮层
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 与 SyncCalendar **功能对齐、形态不同**:
|
|
|
|
|
|
* 它是"跳页面 + bindSheet",家书统一用**页面内浮层**(见 `FbEventSheet` 的说明)。
|
|
|
|
|
|
* ⚠️ 日历本清单走 `/calendar/books`(**真实 id**)——
|
|
|
|
|
|
* 旧代码从条目里臆造 id,导致新建日程根本落不到正确的本上(见 `FbCalRepo.fetchBooks`)。
|
2026-10-11 22:07:48 +08:00
|
|
|
|
*/
|
|
|
|
|
|
|
2026-10-18 20:14:40 +08:00
|
|
|
|
import { FbColors, FbMetric, fbPalette, FbTint } from '../../common/Theme';
|
2026-10-11 22:07:48 +08:00
|
|
|
|
import { FbAppState, fbAppState } from '../../common/Store';
|
2026-10-18 03:35:39 +08:00
|
|
|
|
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_TOKEN_STORE } from '../../data/FbTokenStore';
|
2026-10-18 20:14:40 +08:00
|
|
|
|
import { FbEmptyBox, FbIconButton } from '../../components/Widgets';
|
2026-10-18 03:35:39 +08:00
|
|
|
|
import { FbEventSheet } from '../../components/FbEventSheet';
|
|
|
|
|
|
import { FbEventEditSheet } from '../../components/FbEventEditSheet';
|
|
|
|
|
|
import { FbCalSettingsSheet } from '../../components/FbCalSettingsSheet';
|
|
|
|
|
|
import { FbDayEventPanel } from '../../components/FbDayEventPanel';
|
2026-10-12 12:06:00 +08:00
|
|
|
|
import { FB_DOCK_RESERVE } from '../../components/FloatDock';
|
2026-10-18 03:35:39 +08:00
|
|
|
|
import { fbOnScroll, fbOnTouch } from '../../common/FbScroll';
|
2026-10-11 22:07:48 +08:00
|
|
|
|
|
|
|
|
|
|
/** 月历上「非本月」日期点的灰(hex 常量:格子数据结构里存的是字符串色,不能用 Resource) */
|
2026-10-18 20:14:40 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 列表(和月历)要覆盖的日期范围:今天 -7 ~ 今天 +60 天。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 🔴 这两个数**必须**同时用于两处(改一处就得改另一处):
|
|
|
|
|
|
* ① `load()` 里发给服务端的 `from`/`to`(不传的话服务端只给本月 ⇒ 跨月缺数据);
|
|
|
|
|
|
* ② `agendaDays()` 的循环边界(页面实际渲染哪些天)。
|
|
|
|
|
|
* 分开写死就会变成"请求了 A 段、显示 B 段"——那正是本 bug 最容易复发的形态。
|
|
|
|
|
|
*/
|
|
|
|
|
|
const AGENDA_BACK_DAYS: number = 7;
|
|
|
|
|
|
const AGENDA_FWD_DAYS: number = 60;
|
2026-10-11 22:07:48 +08:00
|
|
|
|
|
|
|
|
|
|
/** 月历格子(一个日期) */
|
|
|
|
|
|
interface FbCalCell {
|
|
|
|
|
|
/** 日期数字 */
|
|
|
|
|
|
day: number;
|
2026-10-18 03:35:39 +08:00
|
|
|
|
/** 该格的完整日期 `YYYY-MM-DD` */
|
|
|
|
|
|
iso: string;
|
2026-10-11 22:07:48 +08:00
|
|
|
|
/** 是否属于当月 */
|
|
|
|
|
|
inMonth: boolean;
|
|
|
|
|
|
/** 是否今天 */
|
|
|
|
|
|
isToday: boolean;
|
2026-10-18 20:14:40 +08:00
|
|
|
|
/* ⚠️ v59:原来的 `count` / `dotColor` 已删除 —— 它们只服务于"日期下面那排
|
|
|
|
|
|
小圆点",而用户 2026-10-18 明确要求**把那排点全部去掉**。
|
|
|
|
|
|
留着这两个字段会让人以为"哪里还在画点",也会让 `cellOf` 白算一遍。 */
|
|
|
|
|
|
|
2026-10-18 03:35:39 +08:00
|
|
|
|
/** 农历/节日的短文字(初一显示月名、节日显示节日名) */
|
|
|
|
|
|
lunar: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 列表视图里的"一天"(日期 + 该日日程) */
|
|
|
|
|
|
interface FbAgendaDay {
|
|
|
|
|
|
iso: string;
|
|
|
|
|
|
/** `10月15日 周三` */
|
|
|
|
|
|
label: string;
|
|
|
|
|
|
events: FbCalEvent[];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 周视图里的一格 */
|
|
|
|
|
|
interface FbWeekDay {
|
|
|
|
|
|
iso: string;
|
|
|
|
|
|
day: number;
|
|
|
|
|
|
/** 一 二 三 … */
|
|
|
|
|
|
w: string;
|
|
|
|
|
|
isToday: boolean;
|
2026-10-18 20:14:40 +08:00
|
|
|
|
/** ⚠️ v59:原来的 `count`(画小点用)已删 —— 用户要求去掉日期下那排点。 */
|
2026-10-18 03:35:39 +08:00
|
|
|
|
lunar: string;
|
2026-10-11 22:07:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@ComponentV2
|
|
|
|
|
|
export struct FbCalendarPage {
|
2026-10-12 10:06:55 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 顶栏插槽:由 Index.ets 传入,放在本页滚动流最前面 ⇒ 随页面一起滚走。
|
|
|
|
|
|
* 🔴 用户要求「向上滑动时标题栏跟着一起滑走」。
|
|
|
|
|
|
*/
|
|
|
|
|
|
@BuilderParam topBar: () => void;
|
2026-10-11 22:07:48 +08:00
|
|
|
|
@Param app: FbAppState = fbAppState();
|
|
|
|
|
|
|
|
|
|
|
|
/** 模块主题色(日程 = 琥珀) */
|
|
|
|
|
|
private accent: string = '#B0763A';
|
|
|
|
|
|
|
2026-10-18 03:35:39 +08:00
|
|
|
|
/** 日历本(走 `/calendar/books`,**真实 id**) */
|
|
|
|
|
|
@Local books: FbCalBook[] = [];
|
|
|
|
|
|
/** 全部条目(月历格子/列表都从这里查) */
|
|
|
|
|
|
@Local allEvents: FbCalEvent[] = [];
|
|
|
|
|
|
/** 顶部提示 */
|
|
|
|
|
|
@Local syncNote: string = '';
|
|
|
|
|
|
/** 加载中 */
|
|
|
|
|
|
@Local loading: boolean = true;
|
|
|
|
|
|
|
|
|
|
|
|
/** 当前视图(月 / 周 / 列表) */
|
|
|
|
|
|
@Local mode: FbCalView = 'month';
|
|
|
|
|
|
/** 日程显示方式(时间轴 / 列表)—— 来自日程设置 */
|
|
|
|
|
|
@Local style: FbCalStyle = 'timeline';
|
|
|
|
|
|
/** 当前显示的月份(月视图) */
|
2026-10-11 22:07:48 +08:00
|
|
|
|
@Local showYear: number = new Date().getFullYear();
|
|
|
|
|
|
@Local showMonth: number = new Date().getMonth() + 1;
|
2026-10-18 03:35:39 +08:00
|
|
|
|
/** 选中的日期 `YYYY-MM-DD` */
|
|
|
|
|
|
@Local pickedIso: string = '';
|
|
|
|
|
|
|
|
|
|
|
|
// ---- 浮层状态 ----
|
|
|
|
|
|
/** 详情:正在看的条目(列表里那份) */
|
|
|
|
|
|
@Local detailBase: FbCalEvent | null = null;
|
|
|
|
|
|
/** 详情:完整数据(异步拉) */
|
|
|
|
|
|
@Local detailFull: FbCalEventFull | null = null;
|
|
|
|
|
|
@Local detailLoading: boolean = false;
|
|
|
|
|
|
@Local detailError: string = '';
|
|
|
|
|
|
/** 编辑:null + 非新建 = 关闭 */
|
|
|
|
|
|
@Local editEvent: FbCalEventFull | null = null;
|
|
|
|
|
|
@Local editing: boolean = false;
|
|
|
|
|
|
/** 新建 */
|
|
|
|
|
|
@Local creating: boolean = false;
|
|
|
|
|
|
/** 设置面板 */
|
|
|
|
|
|
@Local settingsOpen: boolean = false;
|
2026-10-11 22:07:48 +08:00
|
|
|
|
|
|
|
|
|
|
private weekLabels: string[] = ['一', '二', '三', '四', '五', '六', '日'];
|
2026-10-18 03:35:39 +08:00
|
|
|
|
/** 详情请求序号(防"连点两条,前一条的响应覆盖后一条") */
|
|
|
|
|
|
private detailToken: number = 0;
|
|
|
|
|
|
|
|
|
|
|
|
aboutToAppear(): void {
|
|
|
|
|
|
// 先按设置里的默认视图展示(用户上次选的)
|
|
|
|
|
|
this.mode = FB_CAL_SETTINGS.getView();
|
|
|
|
|
|
this.style = FB_CAL_SETTINGS.getStyle();
|
|
|
|
|
|
this.pickedIso = this.todayIso();
|
|
|
|
|
|
this.load();
|
|
|
|
|
|
this.loadBooks();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 四个浮层(详情 / 编辑 / 新建 / 设置)任一开着 → 让底部悬浮快捷栏让位。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 🔴 为什么必须做:悬浮栏在更外层 `Index` 的 Stack 里,层级比本页高 ⇒
|
|
|
|
|
|
* 浮层底部的按钮会被它盖住、点击被抢走(名册页踩过这个坑)。
|
|
|
|
|
|
* ⚠️ 用 `@Monitor` **一处声明**,而不是在每个开合点手写 openSheet() ——
|
|
|
|
|
|
* 手写必然漏(名册页当时有 9 处赋值点)。
|
|
|
|
|
|
* ⚠️ 直接赋 0/1 而不是 `openSheet()/closeSheet()`:本页不会同时开两个浮层,
|
|
|
|
|
|
* 用计数法反而会在"页面重建但旧状态没归零"时把计数越堆越高。
|
|
|
|
|
|
*/
|
|
|
|
|
|
@Monitor('detailBase', 'editEvent', 'creating', 'settingsOpen')
|
|
|
|
|
|
onSheetChanged(): void {
|
|
|
|
|
|
const app: FbAppState = fbAppState();
|
|
|
|
|
|
const open: boolean = this.detailBase !== null || this.editing
|
|
|
|
|
|
|| this.creating || this.settingsOpen;
|
|
|
|
|
|
app.sheetCount = open ? 1 : 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 拉日历本(真实 id / 可写性) */
|
|
|
|
|
|
private async loadBooks(): Promise<void> {
|
|
|
|
|
|
const token: string = FB_TOKEN_STORE.get();
|
|
|
|
|
|
if (token === '') {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const list: FbCalBook[] = await FbCalRepo.inst().fetchBooks(token);
|
|
|
|
|
|
if (list.length > 0) {
|
|
|
|
|
|
this.books = list;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 拉 `/api/v1/calendar`(一次拿全量条目)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 接口没有"按日期查询"的参数(实测整本返回 200+ 条)
|
|
|
|
|
|
* ⇒ 拉一次全量、端上按日期筛;切换月份/视图**不再请求**。
|
|
|
|
|
|
*/
|
|
|
|
|
|
private async load(): Promise<void> {
|
2026-10-18 20:14:40 +08:00
|
|
|
|
/* 🔴🔴 v59(用户 2026-10-18):「列表这个视图,今天的数据缺一条。」
|
|
|
|
|
|
真根因:服务端 `/calendar` 的 from/to **默认只给本月**
|
|
|
|
|
|
(`date('Y-m-01')` ~ `date('Y-m-t')`),而列表视图要显示 `-7 ~ +60` 天。
|
|
|
|
|
|
跨月那一段服务端压根没返回 ⇒ 落在下月的日程(含"今天往后几天")
|
|
|
|
|
|
在列表里凭空少掉;月视图因为本来就只看本月,所以看不出来。
|
|
|
|
|
|
⇒ 这里**显式传范围**,覆盖列表要用的那一整段(前后各留够余量)。
|
|
|
|
|
|
⚠️ 不要只传"本月",也不要依赖服务端默认值 —— 列表与月视图用的
|
|
|
|
|
|
`allEvents` 是同一份,范围必须**取两者所需的并集**(否则修了列表
|
|
|
|
|
|
又让月视图翻页时缺数据)。
|
|
|
|
|
|
⚠️ 往前 7 天 / 往后 60 天这两个数与 `agendaDays()` 是**同一套口径**,
|
|
|
|
|
|
改一处必须改另一处(分散成两个魔法数字必然漂移)。 */
|
|
|
|
|
|
const range: string = '/api/v1/calendar?from=' + this.agendaFrom() + '&to=' + this.agendaTo();
|
2026-10-18 03:35:39 +08:00
|
|
|
|
await loadModule<FbCalEvent[]>('calendar', parseCalendar,
|
|
|
|
|
|
(rows: FbCalEvent[]): void => {
|
|
|
|
|
|
this.allEvents = rows;
|
|
|
|
|
|
this.loading = false;
|
|
|
|
|
|
},
|
|
|
|
|
|
(note: string): void => {
|
|
|
|
|
|
this.syncNote = note;
|
|
|
|
|
|
this.loading = false;
|
2026-10-18 20:14:40 +08:00
|
|
|
|
},
|
|
|
|
|
|
range);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 列表/月历所需范围的起点(今天 - 7 天,`YYYY-MM-DD`) */
|
|
|
|
|
|
private agendaFrom(): string {
|
|
|
|
|
|
return this.isoOfDate(new Date(new Date().getFullYear(), new Date().getMonth(),
|
|
|
|
|
|
new Date().getDate() - AGENDA_BACK_DAYS));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 列表/月历所需范围的终点(今天 + 60 天,`YYYY-MM-DD`) */
|
|
|
|
|
|
private agendaTo(): string {
|
|
|
|
|
|
return this.isoOfDate(new Date(new Date().getFullYear(), new Date().getMonth(),
|
|
|
|
|
|
new Date().getDate() + AGENDA_FWD_DAYS));
|
2026-10-18 03:35:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
// 日期 / 筛选
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
private pad(n: number): string {
|
|
|
|
|
|
return n < 10 ? ('0' + n.toString()) : n.toString();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private todayIso(): string {
|
|
|
|
|
|
const d: Date = new Date();
|
|
|
|
|
|
return d.getFullYear().toString() + '-' + this.pad(d.getMonth() + 1) + '-' + this.pad(d.getDate());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private isoOf(y: number, m: number, d: number): string {
|
|
|
|
|
|
const dt: Date = new Date(y, m - 1, d);
|
|
|
|
|
|
return dt.getFullYear().toString() + '-' + this.pad(dt.getMonth() + 1) + '-' + this.pad(dt.getDate());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-18 20:14:40 +08:00
|
|
|
|
/** `Date` → `YYYY-MM-DD`(服务端 from/to 用的格式) */
|
|
|
|
|
|
private isoOfDate(dt: Date): string {
|
|
|
|
|
|
return this.isoOf(dt.getFullYear(), dt.getMonth() + 1, dt.getDate());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-18 03:35:39 +08:00
|
|
|
|
/** 只看"可见的日历本"里的条目(设置里能逐本关掉) */
|
|
|
|
|
|
private visibleEvents(): FbCalEvent[] {
|
|
|
|
|
|
if (this.books.length === 0) {
|
|
|
|
|
|
return this.allEvents;
|
|
|
|
|
|
}
|
|
|
|
|
|
const out: FbCalEvent[] = [];
|
|
|
|
|
|
for (const e of this.allEvents) {
|
|
|
|
|
|
// calId=0 的衍生条目(农历/纪念日)不受"本"控制,照常显示
|
|
|
|
|
|
if (e.calId === 0 || FB_CAL_SETTINGS.isVisible(e.calId)) {
|
|
|
|
|
|
out.push(e);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return out;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 某一天的条目(已排序) */
|
|
|
|
|
|
private eventsOf(iso: string): FbCalEvent[] {
|
|
|
|
|
|
const out: FbCalEvent[] = [];
|
|
|
|
|
|
for (const e of this.visibleEvents()) {
|
|
|
|
|
|
if (e.date === iso) {
|
|
|
|
|
|
out.push(e);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
out.sort((a: FbCalEvent, c: FbCalEvent): number => this.byTime(a, c));
|
|
|
|
|
|
return out;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private byTime(a: FbCalEvent, c: FbCalEvent): number {
|
|
|
|
|
|
if (a.allDay === c.allDay) {
|
|
|
|
|
|
return a.time < c.time ? -1 : (a.time > c.time ? 1 : 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
return a.allDay ? -1 : 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 选中日期的简短标签(`10月15日 周三`) */
|
|
|
|
|
|
private labelOf(iso: string): string {
|
|
|
|
|
|
if (iso.length < 10) {
|
|
|
|
|
|
return '';
|
|
|
|
|
|
}
|
|
|
|
|
|
const y: number = parseInt(iso.substring(0, 4), 10);
|
|
|
|
|
|
const m: number = parseInt(iso.substring(5, 7), 10);
|
|
|
|
|
|
const d: number = parseInt(iso.substring(8, 10), 10);
|
|
|
|
|
|
const w: string[] = ['日', '一', '二', '三', '四', '五', '六'];
|
|
|
|
|
|
return m.toString() + '月' + d.toString() + '日 周' + w[new Date(y, m - 1, d).getDay()];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 农历/节日的短文字。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 服务端的农历条目有两种形态(实测):
|
|
|
|
|
|
* · 整月一条、`title` 是"农历",`date` 是**初一**那天(`kind=lunar`)
|
|
|
|
|
|
* · 节日/节气各一条(`kind=holiday` / `solarterm` / `traditional`)
|
|
|
|
|
|
* ⇒ 这里**不自己算农历**(那是 `LunarUtil` 的活,且容易算错),
|
|
|
|
|
|
* 直接从条目里取:当天有节日就显示节日名,否则若是初一就显示"农历"当月的标记。
|
|
|
|
|
|
*/
|
|
|
|
|
|
private lunarOf(iso: string): string {
|
|
|
|
|
|
// 优先:传统节日 / 节气 / 节假日(这类名字短、信息量大)
|
|
|
|
|
|
for (const e of this.allEvents) {
|
|
|
|
|
|
if (e.date !== iso) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (e.kind === 'traditional' || e.kind === 'solarterm' || e.kind === 'holiday') {
|
|
|
|
|
|
// 标题可能带 emoji 前缀(如「🎂 婚礼纪念日」不属这类;这里只取纯节日名)
|
|
|
|
|
|
return this.shortTitle(e.title);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-10-18 20:14:40 +08:00
|
|
|
|
// 其次:农历日名
|
|
|
|
|
|
//
|
|
|
|
|
|
// 🔴🔴 2026-10-18 修的真 bug(用户:「农历全部都是初一,说明是错误的数据」):
|
|
|
|
|
|
// 这里原来**硬编码返回 `'初一'`**,注释还写着"`kind=lunar` 的条目只在初一有" ——
|
|
|
|
|
|
// 而事实相反:服务端 `Calendar::month()` 里 `Holiday::lunarRange()` 是
|
|
|
|
|
|
// **每一天都出一条**(`title` = `Lunar::monthName()`(初一显示月名)
|
|
|
|
|
|
// 或 `Lunar::dayName()`),端上把这个真值丢掉了,于是**整月都显示"初一"**。
|
|
|
|
|
|
// ⇒ 直接取服务端给的 title(它本来就是"这一天的农历文字")。
|
|
|
|
|
|
// ⚠️ title 可能带节日后缀(`八月十五 · 中秋`)—— `shortTitle` 截前 3 字,
|
|
|
|
|
|
// 而这张格子里只有 8sp 的位置,本来就该显示短的。
|
2026-10-18 03:35:39 +08:00
|
|
|
|
for (const e of this.allEvents) {
|
|
|
|
|
|
if (e.date === iso && e.kind === 'lunar') {
|
2026-10-18 20:14:40 +08:00
|
|
|
|
const t: string = this.shortTitle(e.title);
|
|
|
|
|
|
if (t !== '') {
|
|
|
|
|
|
return t;
|
|
|
|
|
|
}
|
2026-10-18 03:35:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 标题截短(农历/节日的名字有的很长,格子里放不下) */
|
|
|
|
|
|
private shortTitle(t: string): string {
|
|
|
|
|
|
const s: string = t.replace(/[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}]/gu, '').trim();
|
|
|
|
|
|
return s.length <= 3 ? s : s.substring(0, 3);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 本月条目数(不含农历/节气这类衍生条目,免得数字虚高) */
|
|
|
|
|
|
private monthEventCount(): number {
|
|
|
|
|
|
let n: number = 0;
|
|
|
|
|
|
const prefix: string = this.showYear.toString() + '-' + this.pad(this.showMonth);
|
|
|
|
|
|
for (const e of this.visibleEvents()) {
|
|
|
|
|
|
if (e.date.indexOf(prefix) === 0 && (e.kind === 'custom' || e.kind === 'anniv')) {
|
|
|
|
|
|
n++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return n;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
// 月历格子
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
private cells(): FbCalCell[] {
|
|
|
|
|
|
const out: FbCalCell[] = [];
|
|
|
|
|
|
const y: number = this.showYear;
|
|
|
|
|
|
const m: number = this.showMonth;
|
|
|
|
|
|
|
|
|
|
|
|
const first: Date = new Date(y, m - 1, 1);
|
|
|
|
|
|
// JS 的 getDay(): 0=周日 … 6=周六 ⇒ 换算成「周一为 0」
|
|
|
|
|
|
const lead: number = (first.getDay() + 6) % 7;
|
|
|
|
|
|
const daysInMonth: number = new Date(y, m, 0).getDate();
|
|
|
|
|
|
const prevDays: number = new Date(y, m - 1, 0).getDate();
|
|
|
|
|
|
const today: string = this.todayIso();
|
|
|
|
|
|
|
|
|
|
|
|
// 前置:上月的尾巴
|
|
|
|
|
|
for (let i = lead - 1; i >= 0; i--) {
|
|
|
|
|
|
const d: number = prevDays - i;
|
|
|
|
|
|
out.push(this.cellOf(y, m - 1, d, false, today));
|
|
|
|
|
|
}
|
|
|
|
|
|
// 本月
|
|
|
|
|
|
for (let d = 1; d <= daysInMonth; d++) {
|
|
|
|
|
|
out.push(this.cellOf(y, m, d, true, today));
|
|
|
|
|
|
}
|
|
|
|
|
|
// 补齐到整周(补下月的头)
|
|
|
|
|
|
const tail: number = (7 - (out.length % 7)) % 7;
|
|
|
|
|
|
for (let d = 1; d <= tail; d++) {
|
|
|
|
|
|
out.push(this.cellOf(y, m + 1, d, false, today));
|
|
|
|
|
|
}
|
|
|
|
|
|
return out;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private cellOf(y: number, m: number, d: number, inMonth: boolean, today: string): FbCalCell {
|
|
|
|
|
|
const iso: string = this.isoOf(y, m, d);
|
2026-10-18 20:14:40 +08:00
|
|
|
|
// ⚠️ v59:这里原来还要遍历一遍 events 去数"有几条"(为画小点用)——
|
|
|
|
|
|
// 那排点已被用户要求删除 ⇒ 这次遍历**整段去掉**(格子现在是 O(1) 的)。
|
2026-10-18 03:35:39 +08:00
|
|
|
|
const c: FbCalCell = {
|
|
|
|
|
|
day: d,
|
|
|
|
|
|
iso: iso,
|
|
|
|
|
|
inMonth: inMonth,
|
|
|
|
|
|
isToday: iso === today,
|
|
|
|
|
|
lunar: inMonth ? this.lunarOf(iso) : ''
|
|
|
|
|
|
};
|
|
|
|
|
|
return c;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 网格高度 = 行数 × 行高 */
|
|
|
|
|
|
private gridHeight(): number {
|
|
|
|
|
|
const rows: number = Math.ceil(this.cells().length / 7);
|
|
|
|
|
|
return rows * 48;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
// 周视图
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
/** 本周(周一为首)的 7 天 */
|
|
|
|
|
|
private weekDays(): FbWeekDay[] {
|
|
|
|
|
|
const picked: Date = this.pickedIso.length >= 10
|
|
|
|
|
|
? new Date(parseInt(this.pickedIso.substring(0, 4), 10),
|
|
|
|
|
|
parseInt(this.pickedIso.substring(5, 7), 10) - 1,
|
|
|
|
|
|
parseInt(this.pickedIso.substring(8, 10), 10))
|
|
|
|
|
|
: new Date();
|
|
|
|
|
|
const lead: number = (picked.getDay() + 6) % 7;
|
|
|
|
|
|
const monday: Date = new Date(picked.getFullYear(), picked.getMonth(), picked.getDate() - lead);
|
|
|
|
|
|
const today: string = this.todayIso();
|
|
|
|
|
|
const out: FbWeekDay[] = [];
|
|
|
|
|
|
for (let i = 0; i < 7; i++) {
|
|
|
|
|
|
const dt: Date = new Date(monday.getFullYear(), monday.getMonth(), monday.getDate() + i);
|
|
|
|
|
|
const iso: string = this.isoOf(dt.getFullYear(), dt.getMonth() + 1, dt.getDate());
|
2026-10-18 20:14:40 +08:00
|
|
|
|
// ⚠️ v59:这里原来查一遍 events 只为数"有几条"(画小点用)——
|
|
|
|
|
|
// 那排点已按用户要求删除 ⇒ 整段去掉。
|
2026-10-18 03:35:39 +08:00
|
|
|
|
out.push({
|
|
|
|
|
|
iso: iso, day: dt.getDate(), w: this.weekLabels[i],
|
2026-10-18 20:14:40 +08:00
|
|
|
|
isToday: iso === today, lunar: this.lunarOf(iso)
|
2026-10-18 03:35:39 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
return out;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
// 列表视图
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
/** 从今天往后(含过去 7 天)按天分组;空的日子不占位 */
|
|
|
|
|
|
private agendaDays(): FbAgendaDay[] {
|
|
|
|
|
|
const out: FbAgendaDay[] = [];
|
|
|
|
|
|
const now: Date = new Date();
|
2026-10-18 20:14:40 +08:00
|
|
|
|
// ⚠️ 边界必须与 `load()` 请求的 from/to 一致(见 AGENDA_BACK_DAYS 的说明)
|
|
|
|
|
|
for (let i = -AGENDA_BACK_DAYS; i <= AGENDA_FWD_DAYS; i++) {
|
2026-10-18 03:35:39 +08:00
|
|
|
|
const dt: Date = new Date(now.getFullYear(), now.getMonth(), now.getDate() + i);
|
|
|
|
|
|
const iso: string = this.isoOf(dt.getFullYear(), dt.getMonth() + 1, dt.getDate());
|
|
|
|
|
|
const list: FbCalEvent[] = this.eventsOf(iso);
|
|
|
|
|
|
if (list.length > 0) {
|
|
|
|
|
|
out.push({ iso: iso, label: this.labelOf(iso), events: list });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return out;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
// 浮层开合
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
/** 点一条日程 → 开详情(先把列表那份垫上,再联网补全) */
|
|
|
|
|
|
private openEvent(e: FbCalEvent): void {
|
|
|
|
|
|
this.detailBase = e;
|
|
|
|
|
|
this.detailFull = null;
|
|
|
|
|
|
this.detailError = '';
|
|
|
|
|
|
// 非 custom 的衍生条目(农历/节气)没有 `/events/<id>` 可拉 ⇒ 不请求
|
|
|
|
|
|
if (!isEditableEvent(e)) {
|
|
|
|
|
|
this.detailLoading = false;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const token: string = FB_TOKEN_STORE.get();
|
|
|
|
|
|
if (token === '') {
|
|
|
|
|
|
this.detailLoading = false;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.detailLoading = true;
|
|
|
|
|
|
this.detailToken += 1;
|
|
|
|
|
|
const my: number = this.detailToken;
|
|
|
|
|
|
FbCalRepo.inst().fetchEvent(e.id, token).then((full: FbCalEventFull | null): void => {
|
|
|
|
|
|
if (my !== this.detailToken) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.detailLoading = false;
|
|
|
|
|
|
if (full === null) {
|
|
|
|
|
|
this.detailError = '读不到详情';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.detailFull = full;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 关详情并开编辑 */
|
|
|
|
|
|
private editFromDetail(): void {
|
|
|
|
|
|
if (this.detailFull !== null) {
|
|
|
|
|
|
this.editEvent = this.detailFull;
|
|
|
|
|
|
this.editing = true;
|
|
|
|
|
|
} else if (this.detailBase !== null) {
|
|
|
|
|
|
// 详情还没拉回来 ⇒ 先垫一个"只有列表字段"的壳
|
|
|
|
|
|
const b: FbCalEvent = this.detailBase;
|
|
|
|
|
|
const shell: FbCalEventFull = {
|
|
|
|
|
|
id: b.id, calendar_id: b.calId, title: b.title, location: b.place,
|
|
|
|
|
|
starts_at: b.date + ' ' + (b.allDay ? '00:00:00' : (b.time + ':00')),
|
|
|
|
|
|
ends_at: b.endTime !== '' ? (b.date + ' ' + b.endTime + ':00') : '',
|
|
|
|
|
|
all_day: b.allDay, note: b.note, rrule: '', reminder_minutes: '',
|
|
|
|
|
|
status: 'confirmed', transp: false, created_by: 0
|
|
|
|
|
|
};
|
|
|
|
|
|
this.editEvent = shell;
|
|
|
|
|
|
this.editing = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.detailBase = null;
|
|
|
|
|
|
this.detailFull = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private closeDetail(): void {
|
|
|
|
|
|
this.detailBase = null;
|
|
|
|
|
|
this.detailFull = null;
|
|
|
|
|
|
this.detailError = '';
|
|
|
|
|
|
this.detailToken += 1; // 丢弃在途请求
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 日程系列变了之后刷新数据 */
|
|
|
|
|
|
private refresh(): void {
|
|
|
|
|
|
this.detailBase = null;
|
|
|
|
|
|
this.detailFull = null;
|
|
|
|
|
|
this.editing = false;
|
|
|
|
|
|
this.editEvent = null;
|
|
|
|
|
|
this.creating = false;
|
|
|
|
|
|
this.load();
|
|
|
|
|
|
this.loadBooks();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 详情是不是开着 */
|
|
|
|
|
|
private detailOpen(): boolean {
|
|
|
|
|
|
return this.detailBase !== null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
// 界面
|
|
|
|
|
|
// ============================================================
|
2026-10-11 22:07:48 +08:00
|
|
|
|
|
|
|
|
|
|
build() {
|
2026-10-18 03:35:39 +08:00
|
|
|
|
Stack({ alignContent: Alignment.BottomEnd }) {
|
|
|
|
|
|
// ---- 页面主体(可滚动)----
|
|
|
|
|
|
Scroll() {
|
|
|
|
|
|
Column() {
|
|
|
|
|
|
this.topBar()
|
|
|
|
|
|
this.header()
|
|
|
|
|
|
this.modeTabs()
|
2026-10-11 22:07:48 +08:00
|
|
|
|
|
2026-10-18 03:35:39 +08:00
|
|
|
|
if (this.loading) {
|
|
|
|
|
|
Row({ space: 8 }) {
|
|
|
|
|
|
LoadingProgress().width(18).height(18).color(FbColors.faint)
|
|
|
|
|
|
Text('正在读取日程…')
|
|
|
|
|
|
.fontSize(FbMetric.fontSmall)
|
|
|
|
|
|
.fontColor(FbColors.faint)
|
2026-10-12 10:06:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
2026-10-18 03:35:39 +08:00
|
|
|
|
.padding({ top: 40, bottom: 40 })
|
|
|
|
|
|
.justifyContent(FlexAlign.Center)
|
|
|
|
|
|
} else if (this.mode === 'month') {
|
|
|
|
|
|
this.monthBody()
|
|
|
|
|
|
} else if (this.mode === 'week') {
|
|
|
|
|
|
this.weekBody()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.agendaBody()
|
2026-10-12 10:06:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
2026-10-18 03:35:39 +08:00
|
|
|
|
// 底部悬浮快捷栏 + 这个页面的 + 按钮:留出足够余量,别压住最后一屏
|
|
|
|
|
|
.padding({ bottom: FB_DOCK_RESERVE + 64 })
|
2026-10-11 22:07:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
2026-10-18 03:35:39 +08:00
|
|
|
|
.height('100%')
|
|
|
|
|
|
.scrollBar(BarState.Off)
|
|
|
|
|
|
.edgeEffect(EdgeEffect.Spring)
|
|
|
|
|
|
.onDidScroll(fbOnScroll)
|
|
|
|
|
|
.onTouch(fbOnTouch)
|
|
|
|
|
|
.align(Alignment.TopStart)
|
|
|
|
|
|
|
|
|
|
|
|
// ---- 右下角悬浮 +(照 SyncCalendar)----
|
|
|
|
|
|
Button() {
|
|
|
|
|
|
Text('+')
|
|
|
|
|
|
.fontSize(26)
|
|
|
|
|
|
.fontWeight(FontWeight.Medium)
|
|
|
|
|
|
.fontColor(Color.White)
|
|
|
|
|
|
}
|
|
|
|
|
|
.width(56)
|
|
|
|
|
|
.height(56)
|
|
|
|
|
|
.borderRadius(28)
|
|
|
|
|
|
.backgroundColor(this.accent)
|
|
|
|
|
|
.shadow({ radius: 8, color: '#1A000000', offsetX: 0, offsetY: 2 })
|
|
|
|
|
|
// 底部要给悬浮快捷栏让位(它比这个 + 更靠下)
|
|
|
|
|
|
.margin({ right: 20, bottom: FB_DOCK_RESERVE + 12 })
|
|
|
|
|
|
.onClick((): void => {
|
|
|
|
|
|
this.creating = true;
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// ---- 浮层们 ----
|
|
|
|
|
|
if (this.detailOpen()) {
|
|
|
|
|
|
Column()
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.height('100%')
|
|
|
|
|
|
.backgroundColor('#00000066')
|
|
|
|
|
|
.onClick((): void => {
|
|
|
|
|
|
this.closeDetail();
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
FbEventSheet({
|
|
|
|
|
|
event: this.detailFull,
|
|
|
|
|
|
loading: this.detailLoading,
|
|
|
|
|
|
error: this.detailError,
|
|
|
|
|
|
bookName: this.detailBase !== null ? this.detailBase.calName : '',
|
|
|
|
|
|
bookColor: this.detailBase !== null ? this.detailBase.color : '#9AA0A6',
|
|
|
|
|
|
canEdit: this.detailBase !== null && isEditableEvent(this.detailBase),
|
|
|
|
|
|
onClose: (): void => {
|
|
|
|
|
|
this.closeDetail();
|
|
|
|
|
|
},
|
|
|
|
|
|
onEdit: (): void => {
|
|
|
|
|
|
this.editFromDetail();
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (this.editing && this.editEvent !== null) {
|
|
|
|
|
|
Column()
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.height('100%')
|
|
|
|
|
|
.backgroundColor('#00000066')
|
|
|
|
|
|
.onClick((): void => {
|
|
|
|
|
|
this.editing = false;
|
|
|
|
|
|
this.editEvent = null;
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
FbEventEditSheet({
|
|
|
|
|
|
event: this.editEvent,
|
|
|
|
|
|
isCreate: false,
|
|
|
|
|
|
books: this.books,
|
|
|
|
|
|
onCancel: (): void => {
|
|
|
|
|
|
this.editing = false;
|
|
|
|
|
|
this.editEvent = null;
|
|
|
|
|
|
},
|
|
|
|
|
|
onSaved: (): void => {
|
|
|
|
|
|
this.refresh();
|
|
|
|
|
|
},
|
|
|
|
|
|
onDeleted: (): void => {
|
|
|
|
|
|
this.refresh();
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (this.creating) {
|
|
|
|
|
|
Column()
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.height('100%')
|
|
|
|
|
|
.backgroundColor('#00000066')
|
|
|
|
|
|
.onClick((): void => {
|
|
|
|
|
|
this.creating = false;
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
FbEventEditSheet({
|
|
|
|
|
|
event: null,
|
|
|
|
|
|
isCreate: true,
|
|
|
|
|
|
presetDate: this.pickedIso,
|
|
|
|
|
|
books: this.books,
|
|
|
|
|
|
onCancel: (): void => {
|
|
|
|
|
|
this.creating = false;
|
|
|
|
|
|
},
|
|
|
|
|
|
onSaved: (): void => {
|
|
|
|
|
|
this.refresh();
|
|
|
|
|
|
},
|
|
|
|
|
|
onDeleted: (): void => {
|
|
|
|
|
|
this.creating = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (this.settingsOpen) {
|
|
|
|
|
|
Column()
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.height('100%')
|
|
|
|
|
|
.backgroundColor('#00000066')
|
|
|
|
|
|
.onClick((): void => {
|
|
|
|
|
|
this.settingsOpen = false;
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
FbCalSettingsSheet({
|
|
|
|
|
|
books: this.books,
|
|
|
|
|
|
onClose: (): void => {
|
|
|
|
|
|
this.settingsOpen = false;
|
|
|
|
|
|
},
|
|
|
|
|
|
onChanged: (): void => {
|
|
|
|
|
|
// 设置改了 ⇒ 立刻按新设置重画(换视图 / 换显示方式马上生效)
|
|
|
|
|
|
this.mode = FB_CAL_SETTINGS.getView();
|
|
|
|
|
|
this.style = FB_CAL_SETTINGS.getStyle();
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
2026-10-11 22:07:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.height('100%')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-18 03:35:39 +08:00
|
|
|
|
/** 页头:月份标题 + 「今天」 + 右侧设置齿轮 */
|
|
|
|
|
|
@Builder
|
|
|
|
|
|
header() {
|
|
|
|
|
|
Row({ space: 8 }) {
|
|
|
|
|
|
Column({ space: 2 }) {
|
|
|
|
|
|
if (this.mode === 'month') {
|
|
|
|
|
|
Text(this.showYear.toString() + ' 年 ' + this.showMonth.toString() + ' 月')
|
|
|
|
|
|
.fontSize(FbMetric.fontTitle)
|
|
|
|
|
|
.fontWeight(FontWeight.Medium)
|
|
|
|
|
|
.fontColor(FbColors.ink)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Text(this.labelOf(this.pickedIso))
|
|
|
|
|
|
.fontSize(FbMetric.fontTitle)
|
|
|
|
|
|
.fontWeight(FontWeight.Medium)
|
|
|
|
|
|
.fontColor(FbColors.ink)
|
|
|
|
|
|
}
|
|
|
|
|
|
Text(this.visibleBooks().length.toString() + ' 本日历可见 · 本月 '
|
|
|
|
|
|
+ this.monthEventCount().toString() + ' 条')
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(FbColors.faint)
|
|
|
|
|
|
}
|
|
|
|
|
|
.alignItems(HorizontalAlign.Start)
|
|
|
|
|
|
.layoutWeight(1)
|
|
|
|
|
|
|
|
|
|
|
|
// 「今天」(照 SyncCalendar 那颗:文字按钮、品牌色描边)
|
|
|
|
|
|
Text('今天')
|
|
|
|
|
|
.fontSize(FbMetric.fontSmall)
|
|
|
|
|
|
.fontColor(this.accent)
|
|
|
|
|
|
.padding({ left: 12, right: 12, top: 5, bottom: 5 })
|
|
|
|
|
|
.border({ width: 1, color: this.accent })
|
|
|
|
|
|
.borderRadius(FbMetric.radiusPill)
|
|
|
|
|
|
.onClick((): void => {
|
|
|
|
|
|
const now: Date = new Date();
|
|
|
|
|
|
this.showYear = now.getFullYear();
|
|
|
|
|
|
this.showMonth = now.getMonth() + 1;
|
|
|
|
|
|
this.pickedIso = this.todayIso();
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 🔴 设置按钮 = 网页版那种**三条横线**(☰)—— 用户 2026-10-16:
|
|
|
|
|
|
// 「所有页面的设置按钮全部都改成和网页版一致」。
|
|
|
|
|
|
// (网页版日程页的 `_settings_menu.php` 用的就是那个三横线 SVG。)
|
|
|
|
|
|
FbIconButton({
|
|
|
|
|
|
icon: $r('app.media.fb_ic_menu'),
|
2026-10-18 20:14:40 +08:00
|
|
|
|
tint: FbTint.muted,
|
2026-10-18 03:35:39 +08:00
|
|
|
|
onTap: (): void => {
|
|
|
|
|
|
this.settingsOpen = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12, bottom: 8 })
|
|
|
|
|
|
.alignItems(VerticalAlign.Center)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 胶囊分段:月 / 周 / 列表(照 SyncCalendar 的 modeTabs) */
|
|
|
|
|
|
@Builder
|
|
|
|
|
|
modeTabs() {
|
|
|
|
|
|
Row({ space: 6 }) {
|
|
|
|
|
|
this.modeTab('month', '月')
|
|
|
|
|
|
this.modeTab('week', '周')
|
|
|
|
|
|
this.modeTab('agenda', '列表')
|
|
|
|
|
|
Blank()
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding({ left: FbMetric.pagePad, right: FbMetric.pagePad, bottom: 8 })
|
|
|
|
|
|
.alignItems(VerticalAlign.Center)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Builder
|
|
|
|
|
|
modeTab(key: string, label: string) {
|
|
|
|
|
|
Text(label)
|
|
|
|
|
|
.fontSize(FbMetric.fontSmall)
|
|
|
|
|
|
.fontColor(this.mode === key ? Color.White : FbColors.muted)
|
|
|
|
|
|
.backgroundColor(this.mode === key ? this.accent : FbColors.inputBg)
|
|
|
|
|
|
.borderRadius(FbMetric.radiusPill)
|
|
|
|
|
|
.padding({ left: 16, right: 16, top: 6, bottom: 6 })
|
|
|
|
|
|
.onClick((): void => {
|
|
|
|
|
|
this.mode = key as FbCalView;
|
|
|
|
|
|
// ⚠️ 手动切换**也**存成默认视图 —— 用户下次进来就是他最后用的那个
|
|
|
|
|
|
// (SyncCalendar 只存设置页里选的,用起来反而别扭)
|
|
|
|
|
|
FB_CAL_SETTINGS.setView(this.mode);
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 月视图:月历卡 + 选中日日程 */
|
|
|
|
|
|
@Builder
|
|
|
|
|
|
monthBody() {
|
|
|
|
|
|
Column({ space: 0 }) {
|
|
|
|
|
|
// ---- 月历卡 ----
|
|
|
|
|
|
Column({ space: 0 }) {
|
|
|
|
|
|
// 月份切换条
|
|
|
|
|
|
Row() {
|
|
|
|
|
|
this.navBtn('上一月', -1)
|
|
|
|
|
|
Blank()
|
|
|
|
|
|
this.navBtn('下一月', 1)
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding({ bottom: 10 })
|
|
|
|
|
|
|
|
|
|
|
|
// 星期表头
|
|
|
|
|
|
Row() {
|
|
|
|
|
|
ForEach(this.weekLabels, (w: string) => {
|
|
|
|
|
|
Text(w)
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(FbColors.faint)
|
|
|
|
|
|
.layoutWeight(1)
|
|
|
|
|
|
.textAlign(TextAlign.Center)
|
|
|
|
|
|
}, (w: string): string => 'wk_' + w)
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding({ bottom: 6 })
|
|
|
|
|
|
|
|
|
|
|
|
// 日期网格(7 列)
|
|
|
|
|
|
// 🔴 key 里带上 `this.pickedIso`:格子的选中态是**状态派生**的,
|
|
|
|
|
|
// 而 key 若只含日期,切选中日时 key 不变 ⇒ ArkUI 会复用旧子组件。
|
|
|
|
|
|
// (`dayCell` 内部确实读了 `this.pickedIso`,理论上能刷新;
|
|
|
|
|
|
// 但把选中态并进 key 是"一定能重建"的稳妥做法,
|
|
|
|
|
|
// 42 个格子的重建开销可忽略。)
|
|
|
|
|
|
Grid() {
|
|
|
|
|
|
ForEach(this.cells(), (c: FbCalCell) => {
|
|
|
|
|
|
GridItem() {
|
|
|
|
|
|
this.dayCell(c)
|
|
|
|
|
|
}
|
|
|
|
|
|
}, (c: FbCalCell): string => 'cell_' + c.iso + '_' + this.pickedIso)
|
|
|
|
|
|
}
|
|
|
|
|
|
.columnsTemplate('1fr 1fr 1fr 1fr 1fr 1fr 1fr')
|
|
|
|
|
|
.rowsGap(2)
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.height(this.gridHeight())
|
|
|
|
|
|
.scrollBar(BarState.Off)
|
|
|
|
|
|
}
|
|
|
|
|
|
.padding(FbMetric.cardPad)
|
|
|
|
|
|
.backgroundColor(FbColors.surface)
|
|
|
|
|
|
.borderRadius(FbMetric.radius)
|
|
|
|
|
|
.margin({ left: FbMetric.pagePad, right: FbMetric.pagePad })
|
|
|
|
|
|
|
|
|
|
|
|
// ---- 选中日的日程 ----
|
|
|
|
|
|
this.dayPanel()
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 选中日的日程面板(月/周视图共用)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 🔴🔴 v53 修的真 bug(用户 2026-10-16):「你在日历上点击任何一个地方,
|
|
|
|
|
|
* **下面的日程都不变**。」
|
|
|
|
|
|
* 根因:这里原来是 `@Builder dayPanel(iso: string)` ——
|
|
|
|
|
|
* **`@Builder` 的值类型参数不参与状态观察**
|
|
|
|
|
|
* (SDK 规则 `r/quick-rules/05-state.md`:按值传递「状态变化刷新 ❌」)。
|
|
|
|
|
|
* 关键在**渲染时"读了什么"**:
|
|
|
|
|
|
* · 旧写法里这块只读了入参 `iso`(一个普通局部值),
|
|
|
|
|
|
* 整棵子树的依赖里**没有 `this.pickedIso`**
|
|
|
|
|
|
* ⇒ 点格子后 `pickedIso` 变了,这块**根本不被重新渲染**。
|
|
|
|
|
|
* · 对照 `dayCell(c)` 之所以正常:它在渲染时**直接读了 `this.pickedIso`**
|
|
|
|
|
|
* (`c.iso === this.pickedIso`)⇒ 依赖被登记 ⇒ 选中高亮会跟。
|
|
|
|
|
|
* ⇒ 修法:**去掉那个参数**,让它在渲染时直接读 `this.pickedIso`
|
|
|
|
|
|
* (这样依赖被登记);外面套的是 `@ComponentV2` 的 `FbDayEventPanel`,
|
|
|
|
|
|
* `@Param` 一变就重建。本项目已在 `FbTagChip` 上踩过同一个坑。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 所以**不要**把它改回 `dayPanel(iso: string)` 再传 `this.pickedIso` ——
|
|
|
|
|
|
* 那样又变成按值传递,症状会原样复发。
|
|
|
|
|
|
*/
|
|
|
|
|
|
@Builder
|
|
|
|
|
|
dayPanel() {
|
|
|
|
|
|
FbDayEventPanel({
|
|
|
|
|
|
label: this.labelOf(this.pickedIso),
|
|
|
|
|
|
events: this.eventsOf(this.pickedIso),
|
|
|
|
|
|
style: this.style,
|
|
|
|
|
|
isToday: this.pickedIso === this.todayIso(),
|
|
|
|
|
|
nowMin: this.nowMinutes(),
|
|
|
|
|
|
onPick: (e: FbCalEvent): void => {
|
|
|
|
|
|
this.openEvent(e);
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 当前时刻(当天第几分钟)—— 时间轴画"当前时间红线"用 */
|
|
|
|
|
|
private nowMinutes(): number {
|
|
|
|
|
|
const d: Date = new Date();
|
|
|
|
|
|
return d.getHours() * 60 + d.getMinutes();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 周视图:7 列日期条 + 选中日日程 */
|
|
|
|
|
|
@Builder
|
|
|
|
|
|
weekBody() {
|
|
|
|
|
|
Column({ space: 0 }) {
|
|
|
|
|
|
Column({ space: 0 }) {
|
|
|
|
|
|
Row() {
|
|
|
|
|
|
this.navBtn('上一周', -7)
|
|
|
|
|
|
Blank()
|
|
|
|
|
|
this.navBtn('下一周', 7)
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.padding({ bottom: 10 })
|
|
|
|
|
|
|
|
|
|
|
|
// 7 列
|
|
|
|
|
|
Row({ space: 2 }) {
|
|
|
|
|
|
ForEach(this.weekDays(), (d: FbWeekDay) => {
|
|
|
|
|
|
Column({ space: 4 }) {
|
|
|
|
|
|
Text(d.w)
|
|
|
|
|
|
.fontSize(FbMetric.fontTiny)
|
|
|
|
|
|
.fontColor(FbColors.faint)
|
2026-10-18 20:14:40 +08:00
|
|
|
|
/* 🔴 v59:选中 / 今天的配色与月视图**同一套**(选中深底白字、
|
|
|
|
|
|
今天浅底)—— 用户要的是"点哪天哪天明显",两处不一致会让人
|
|
|
|
|
|
以为换视图就丢了选中。判据同样走方法(见 `isPickedCell` 的说明)。 */
|
2026-10-18 03:35:39 +08:00
|
|
|
|
Text(d.day.toString())
|
|
|
|
|
|
.fontSize(FbMetric.fontSmall)
|
2026-10-18 20:14:40 +08:00
|
|
|
|
.fontWeight(d.iso === this.pickedIso || d.isToday
|
2026-10-18 03:35:39 +08:00
|
|
|
|
? FontWeight.Medium : FontWeight.Normal)
|
2026-10-18 20:14:40 +08:00
|
|
|
|
.fontColor(d.iso === this.pickedIso
|
|
|
|
|
|
? Color.White
|
|
|
|
|
|
: FbColors.ink)
|
2026-10-18 03:35:39 +08:00
|
|
|
|
.width(30)
|
|
|
|
|
|
.height(30)
|
|
|
|
|
|
.textAlign(TextAlign.Center)
|
|
|
|
|
|
.borderRadius(15)
|
2026-10-18 20:14:40 +08:00
|
|
|
|
.backgroundColor(d.iso === this.pickedIso
|
2026-10-18 03:35:39 +08:00
|
|
|
|
? this.accent
|
2026-10-18 20:14:40 +08:00
|
|
|
|
: (d.isToday ? fbPalette(this.accent).wash : Color.Transparent))
|
|
|
|
|
|
/* 🔴 v59(用户 2026-10-18):「日期下面有几个点,合格都去掉。」
|
|
|
|
|
|
⇒ 周视图这一排小圆点也删(同一条要求,别只改月视图)。
|
|
|
|
|
|
⚠️ 留等高占位,避免周条高度变化。 */
|
|
|
|
|
|
Row().height(5)
|
2026-10-18 03:35:39 +08:00
|
|
|
|
if (d.lunar !== '') {
|
|
|
|
|
|
Text(d.lunar)
|
|
|
|
|
|
.fontSize(8)
|
|
|
|
|
|
.fontColor(FbColors.faint)
|
|
|
|
|
|
.maxLines(1)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
.layoutWeight(1)
|
|
|
|
|
|
.padding({ top: 4, bottom: 4 })
|
|
|
|
|
|
.onClick((): void => {
|
|
|
|
|
|
this.pickedIso = d.iso;
|
|
|
|
|
|
})
|
|
|
|
|
|
}, (d: FbWeekDay): string => 'wkd_' + d.iso)
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
.alignItems(VerticalAlign.Top)
|
|
|
|
|
|
}
|
|
|
|
|
|
.padding(FbMetric.cardPad)
|
|
|
|
|
|
.backgroundColor(FbColors.surface)
|
|
|
|
|
|
.borderRadius(FbMetric.radius)
|
|
|
|
|
|
.margin({ left: FbMetric.pagePad, right: FbMetric.pagePad })
|
|
|
|
|
|
|
|
|
|
|
|
this.dayPanel()
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 列表视图:按天分组,从今天往后(每天一块,显示方式同样跟随设置) */
|
|
|
|
|
|
@Builder
|
|
|
|
|
|
agendaBody() {
|
|
|
|
|
|
Column({ space: 0 }) {
|
|
|
|
|
|
if (this.agendaDays().length === 0) {
|
|
|
|
|
|
Column() {
|
|
|
|
|
|
FbEmptyBox({
|
|
|
|
|
|
title: '接下来还没有安排',
|
|
|
|
|
|
hint: '点右下角的 + 加一条日程',
|
|
|
|
|
|
icon: $r('app.media.fb_ic_calendar')
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
.backgroundColor(FbColors.surface)
|
|
|
|
|
|
.borderRadius(FbMetric.radius)
|
|
|
|
|
|
.padding(FbMetric.cardPad)
|
|
|
|
|
|
.margin({ left: FbMetric.pagePad, right: FbMetric.pagePad })
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ForEach(this.agendaDays(), (g: FbAgendaDay) => {
|
|
|
|
|
|
// 🔴 与月/周视图共用同一个面板组件(这样"显示方式"设置在三处都生效)
|
|
|
|
|
|
FbDayEventPanel({
|
|
|
|
|
|
label: g.label,
|
|
|
|
|
|
events: g.events,
|
|
|
|
|
|
style: this.style,
|
|
|
|
|
|
isToday: g.iso === this.todayIso(),
|
|
|
|
|
|
nowMin: this.nowMinutes(),
|
|
|
|
|
|
onPick: (e: FbCalEvent): void => {
|
|
|
|
|
|
this.openEvent(e);
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}, (g: FbAgendaDay): string => 'agd_' + g.iso)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 上一月 / 下一月(周视图里步进 7 天) */
|
2026-10-11 22:07:48 +08:00
|
|
|
|
@Builder
|
|
|
|
|
|
navBtn(label: string, delta: number) {
|
|
|
|
|
|
Text(label)
|
|
|
|
|
|
.fontSize(FbMetric.fontSmall)
|
|
|
|
|
|
.fontColor(FbColors.muted)
|
|
|
|
|
|
.padding({ left: 12, right: 12, top: 5, bottom: 5 })
|
|
|
|
|
|
.backgroundColor(FbColors.inputBg)
|
|
|
|
|
|
.borderRadius(FbMetric.radiusPill)
|
|
|
|
|
|
.onClick(() => {
|
2026-10-18 03:35:39 +08:00
|
|
|
|
if (this.mode === 'week') {
|
|
|
|
|
|
// 周视图:按天挪(±7)
|
|
|
|
|
|
const base: Date = this.pickedIso.length >= 10
|
|
|
|
|
|
? new Date(parseInt(this.pickedIso.substring(0, 4), 10),
|
|
|
|
|
|
parseInt(this.pickedIso.substring(5, 7), 10) - 1,
|
|
|
|
|
|
parseInt(this.pickedIso.substring(8, 10), 10))
|
|
|
|
|
|
: new Date();
|
|
|
|
|
|
const dt: Date = new Date(base.getFullYear(), base.getMonth(), base.getDate() + delta);
|
|
|
|
|
|
this.pickedIso = this.isoOf(dt.getFullYear(), dt.getMonth() + 1, dt.getDate());
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 月视图:按月挪
|
2026-10-11 22:07:48 +08:00
|
|
|
|
let m: number = this.showMonth + delta;
|
|
|
|
|
|
let y: number = this.showYear;
|
|
|
|
|
|
if (m < 1) {
|
|
|
|
|
|
m = 12;
|
|
|
|
|
|
y = y - 1;
|
|
|
|
|
|
} else if (m > 12) {
|
|
|
|
|
|
m = 1;
|
|
|
|
|
|
y = y + 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.showMonth = m;
|
|
|
|
|
|
this.showYear = y;
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-18 03:35:39 +08:00
|
|
|
|
/** 一个日期格子(照 SyncCalendar:日号 + 农历,选中/今天高亮) */
|
2026-10-11 22:07:48 +08:00
|
|
|
|
@Builder
|
|
|
|
|
|
dayCell(c: FbCalCell) {
|
2026-10-18 03:35:39 +08:00
|
|
|
|
Column({ space: 2 }) {
|
2026-10-18 20:14:40 +08:00
|
|
|
|
/* 🔴🔴 v59(用户 2026-10-18):「现在点击了某一天之后,并不明显,
|
|
|
|
|
|
应该把这一天的颜色变深,而今天用一个较浅的颜色来显示,
|
|
|
|
|
|
你参考 synccalendar 的做法。」
|
|
|
|
|
|
⇒ 照本机范本 SyncCalendar 的 `weekStrip`:
|
|
|
|
|
|
· **选中** → `selected_bg` **深色实底** + 白字(最强调);
|
|
|
|
|
|
· **今天** → `today_bg` **浅色底** + 常规文字色(弱强调)。
|
|
|
|
|
|
原来的写法正好**反了**:今天用实底(`this.accent` + 白字)抢走了
|
|
|
|
|
|
全部注意力,而选中的那天只有一层 12% 的淡色 —— 点下去几乎看不出变化。
|
|
|
|
|
|
⚠️ 两个都免得同时成立(选中的那天可能也是今天):**选中优先**,
|
|
|
|
|
|
否则"我今天点了今天,却看不到选中效果"。
|
|
|
|
|
|
⚠️ 深浅对照是**这套配色的核心**:选中实底、今天浅底 —— 别再改回去。 */
|
2026-10-11 22:07:48 +08:00
|
|
|
|
Text(c.day.toString())
|
|
|
|
|
|
.fontSize(FbMetric.fontSmall)
|
2026-10-18 20:14:40 +08:00
|
|
|
|
.fontWeight(this.isPickedCell(c) || c.isToday ? FontWeight.Medium : FontWeight.Normal)
|
|
|
|
|
|
.fontColor(this.isPickedCell(c)
|
2026-10-11 22:07:48 +08:00
|
|
|
|
? Color.White
|
|
|
|
|
|
: (c.inMonth ? FbColors.ink : FbColors.faint))
|
|
|
|
|
|
.width(26)
|
|
|
|
|
|
.height(26)
|
|
|
|
|
|
.textAlign(TextAlign.Center)
|
|
|
|
|
|
.borderRadius(13)
|
2026-10-18 20:14:40 +08:00
|
|
|
|
.backgroundColor(this.isPickedCell(c)
|
|
|
|
|
|
? this.accent // 选中:深色实底
|
|
|
|
|
|
: (c.isToday ? fbPalette(this.accent).wash // 今天:浅色底
|
2026-10-11 22:07:48 +08:00
|
|
|
|
: Color.Transparent))
|
|
|
|
|
|
|
2026-10-18 03:35:39 +08:00
|
|
|
|
// 农历/节日(有才显示)
|
|
|
|
|
|
if (c.lunar !== '') {
|
|
|
|
|
|
Text(c.lunar)
|
|
|
|
|
|
.fontSize(8)
|
2026-10-18 20:14:40 +08:00
|
|
|
|
.fontColor(this.isPickedCell(c) && c.inMonth ? FbColors.muted : FbColors.faint)
|
2026-10-18 03:35:39 +08:00
|
|
|
|
.maxLines(1)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 没有农历就留出等高的空,免得两行格子和一行格子高度不一
|
|
|
|
|
|
Row().height(10)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-18 20:14:40 +08:00
|
|
|
|
/* 🔴 v59(用户 2026-10-18):「日程,日期下面有几个点,合格都去掉。」
|
|
|
|
|
|
⇒ 那一排小圆点**已删除**(连同 `FbDot` 的引用与 `dots()` 辅助函数
|
|
|
|
|
|
在月视图的调用点)。用户认为它是噪音 —— 日程内容在下面那块
|
|
|
|
|
|
「选中日的日程」里看得更清楚,格子里不需要再暗示"有几条"。
|
|
|
|
|
|
⚠️ 周视图的圆点也一并去掉(同一条要求),别只删月视图。
|
|
|
|
|
|
⚠️ 这里**保留等高占位**:不留的话格子高度会变,7 行月历会整体上移。 */
|
|
|
|
|
|
Row().height(5)
|
2026-10-11 22:07:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
.width('100%')
|
2026-10-18 03:35:39 +08:00
|
|
|
|
.height(48)
|
2026-10-11 22:07:48 +08:00
|
|
|
|
.justifyContent(FlexAlign.Center)
|
|
|
|
|
|
.alignItems(HorizontalAlign.Center)
|
|
|
|
|
|
.onClick(() => {
|
2026-10-18 03:35:39 +08:00
|
|
|
|
this.pickedIso = c.iso;
|
|
|
|
|
|
// 点到上/下月的格子 ⇒ 顺带把月历翻过去(SyncCalendar 也是这个行为)
|
|
|
|
|
|
if (!c.inMonth && c.iso.length >= 10) {
|
|
|
|
|
|
this.showYear = parseInt(c.iso.substring(0, 4), 10);
|
|
|
|
|
|
this.showMonth = parseInt(c.iso.substring(5, 7), 10);
|
|
|
|
|
|
}
|
2026-10-11 22:07:48 +08:00
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-18 20:14:40 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 这一格是不是"当前选中的那天"。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 必须是**方法调用**(build 里读 `this.pickedIso`)而不是把结果当参数传进
|
|
|
|
|
|
* `dayCell` —— 后者按值传递,@Builder 的多值参数不参与观察,
|
|
|
|
|
|
* 选中高亮会**永远停在初始那天**(本项目已在 FbTagChip 上踩过同一个坑,
|
|
|
|
|
|
* 见本文件 `dayPanel()` 上那段说明)。
|
|
|
|
|
|
*/
|
|
|
|
|
|
private isPickedCell(c: FbCalCell): boolean {
|
|
|
|
|
|
return c.inMonth && c.iso === this.pickedIso;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-18 03:35:39 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* ⚠️ 日程条目行(列表模式)已经搬进 `FbDayEventPanel` 组件里
|
|
|
|
|
|
* (月/周/列表三处共用同一份 + 跟随"显示方式"设置)——
|
|
|
|
|
|
* 这里不再保留旧的 `@Builder eventRow`(那是 `@Builder` 值参数不刷新的老写法)。
|
|
|
|
|
|
*/
|
2026-10-11 22:07:48 +08:00
|
|
|
|
|
2026-10-18 03:35:39 +08:00
|
|
|
|
/** 只列「显示着的」日历本 */
|
|
|
|
|
|
private visibleBooks(): FbCalBook[] {
|
|
|
|
|
|
const out: FbCalBook[] = [];
|
|
|
|
|
|
for (const b of this.books) {
|
|
|
|
|
|
if (FB_CAL_SETTINGS.isVisible(b.id)) {
|
|
|
|
|
|
out.push(b);
|
2026-10-11 22:07:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-10-18 03:35:39 +08:00
|
|
|
|
return out;
|
2026-10-11 22:07:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|