feat(web): 日历前端 —— 月/周/日三粒度 + 事件编辑器 + 农历显示
布局与全站一致:**内容区自己再分两栏**。 第一版是单栏 —— 网格铺满整个主区域,宽屏下右边一大片空白无事可做, 而点「新建」时编辑器**顶掉**整个日历,人失去正在看的那个月的上下文。 两个问题同源:日历没有「详情栏」这个位置。 现在左边网格、右边常驻面板:默认显示选中那天的日程(所以永远不空), 新建/编辑时同一位置变编辑器 —— 与「新建邮件是右侧整页」同一套语言。 窄屏退回覆盖式(NarrowStack,与收件箱详情同一组件同一段动画)。 lib/calendar.ts 里三个易错点(都有测试钉住): **周首必须是周一。** getDay() 把周日算作 0,直接减它会让周日归到上一周 末尾,月视图第一行整体错位。 **分桶用本地日期串而不是 toISOString().slice(0,10)。** 后者给 UTC 日期, 东八区晚上 8 点后的事件会被归到第二天的格子里。 **月视图固定 42 格。** 按需 4~6 行会让网格高度随月份跳动,翻月时页面弹动。 另有一个 TS 陷阱:replaceAll 在 tsconfig 的 target: ES2020 下不存在 (TS2550)。改用 split/join —— **不能**退回 replace,那只换第一个, 同一变量写两次时第二个会原样漏进邮件。 编辑器: - 农历规则预览**接下来三次的公历日期**。规则名(「每农历月廿二」)看不出 公历日子,而那日子每次都在变 —— 不预览的话人要等一个月才知道理解对没对。 - 变量按钮 + 实时渲染预览。「模板里写了什么」和「Agent 收到什么」不是一个 东西,不给预览人只能发一次试试看。 - 收件人列表可增删调序(together 模式下首个是主收件人,顺序有语义)。 编辑旧事件时初值走与后端相同的兜底链 —— 不做归一化的话,编辑一条老事件 再保存会把收件人清空(列表是空的,保存就覆盖了)。 - remindAt 单独显示:调度器比较的是「事件时间 − 提前分钟」那个点, 不显示的话人设了「提前 30 分钟」却在事件时间才反应过来。 日历格子标农历日(初一显示月名,纸质日历惯例)。暂停/取消的事件加删除线 与图标 —— 否则人以为设好了,实际到点什么都不会发生。 exportCalendarICS 不走 request()(那个假定 JSON,这里是 text/calendar), 返回文本让调用方用 Blob 触发下载而不是让浏览器导航 —— 导航会丢掉 cookie 之外的认证头。 测试 35 例。
This commit is contained in:
@ -246,3 +246,109 @@ export interface AdminScopes {
|
||||
agents: string[];
|
||||
paths: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 日历事件。
|
||||
*
|
||||
* 三层分离:事件是日历实体,提醒是触发器,邮件是投递通道。
|
||||
* 提醒到点时调度器发一封 from_name="calendar" 的邮件给 to_address ——
|
||||
* 对 Agent 来说就是一封普通邮件,它不知道也不需要知道信来自日历。
|
||||
*/
|
||||
export interface CalendarEvent {
|
||||
event_id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
/**
|
||||
* 提醒邮件的正文。留空时后端按 title/time/description 生成默认模板。
|
||||
* 支持 {title} {time} {description} 三个变量,触发时替换。
|
||||
*/
|
||||
reminder_text: string;
|
||||
/**
|
||||
* 收件 Agent 名 / 完整地址 —— **单收件人时代的字段**。
|
||||
* 保留作兼容与兜底:recipients 为空时才用它们。
|
||||
* 新代码一律用 effectiveRecipients()。
|
||||
*/
|
||||
agent_name: string;
|
||||
to_address: string;
|
||||
/**
|
||||
* 收件人列表,每项是完整三维地址串。
|
||||
*
|
||||
* 存原始串而不是结构化地址:session 位的 new/别名三态该在**触发那一刻**
|
||||
* 解析。存结构化的话「.new」这种一次性语义在建事件时就被固化,
|
||||
* 而重复事件每次触发都该重新决定落到哪条会话。
|
||||
*/
|
||||
recipients: string[];
|
||||
/**
|
||||
* 多收件人的投递方式。
|
||||
* separate = 各发一封、落各自会话、互相看不到
|
||||
* together = 首个为主收件人,其余进抄送、共享同一条线索
|
||||
*
|
||||
* 两种都要而不是二选一:「三个 Agent 各自独立汇报」与「pi 主办、dsh 知情」
|
||||
* 是完全不同的任务形态。用错 together 会让本该独立判断的 Agent 互相
|
||||
* 看到回复而趋同,那种污染事后无法分离。
|
||||
*/
|
||||
delivery_mode: DeliveryMode;
|
||||
/** ISO 8601 */
|
||||
event_time: string;
|
||||
/** 提前多少分钟提醒;0 = 到点才提醒 */
|
||||
remind_before: number;
|
||||
recurrence: Recurrence;
|
||||
/** 重复终止时间;越过它事件自动置为 cancelled */
|
||||
recurrence_end?: string;
|
||||
status: 'active' | 'paused' | 'cancelled';
|
||||
/** 上次触发的墙上时钟 */
|
||||
last_fired_at?: string;
|
||||
/**
|
||||
* 已触发的那个 occurrence(值 = 当时的 event_time)。
|
||||
* 去重靠它与 event_time 相等判断,不是拿 last_fired_at 比大小 ——
|
||||
* 后端 DueEvents 有 60 秒 lookahead,后者在窗口内恒为真会导致每 tick 重发。
|
||||
*/
|
||||
fired_for?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
created_by: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重复规则。农历两种单独存在,因为它们的公历日期每年都在漂移 ——
|
||||
* 用公历 yearly 会固定在同一天,与「过农历生日/祭日」的期望不符。
|
||||
*
|
||||
* 没有 lunar_daily(农历的「日」与公历同长,那就是 daily)
|
||||
* 也没有 lunar_weekly(农历没有「周」这个单位)。
|
||||
*/
|
||||
export type Recurrence =
|
||||
| 'none'
|
||||
| 'daily'
|
||||
| 'weekly'
|
||||
| 'monthly'
|
||||
| 'yearly'
|
||||
| 'lunar_monthly'
|
||||
| 'lunar_yearly';
|
||||
|
||||
export type DeliveryMode = 'separate' | 'together';
|
||||
|
||||
/** 事件附件(随提醒邮件一起发出) */
|
||||
export interface CalendarAttachment {
|
||||
attachment_id: string;
|
||||
event_id: string;
|
||||
filename: string;
|
||||
sha256: string;
|
||||
size_bytes: number;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
/** 新建/编辑事件的请求体。event_time 必填,其余可省。 */
|
||||
export interface CalendarEventInput {
|
||||
title: string;
|
||||
description?: string;
|
||||
reminder_text?: string;
|
||||
agent_name?: string;
|
||||
to_address?: string;
|
||||
recipients?: string[];
|
||||
delivery_mode?: DeliveryMode;
|
||||
event_time: string;
|
||||
remind_before?: number;
|
||||
recurrence?: Recurrence;
|
||||
recurrence_end?: string | null;
|
||||
status?: 'active' | 'paused' | 'cancelled';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user