侧边栏插件

侧边栏插件为 DayFlow 日历添加了一个日历管理侧边栏。它包括一个用于日期导航的迷你日历、带可见性开关的日历列表,以及完整的日历 CRUD 操作(创建、重命名、更改颜色、合并、删除、导入/导出)。

安装

安装插件包:

npm install @dayflow/plugin-sidebar

使用

import { useCalendarApp, DayFlowCalendar } from '@dayflow/react';
import { createSidebarPlugin } from '@dayflow/plugin-sidebar';

function MyCalendar() {
  const sidebarPlugin = createSidebarPlugin({
    width: 280,
    createCalendarMode: 'modal',
  });

  const calendar = useCalendarApp({
    views: [
      /* 视图 */
    ],
    plugins: [sidebarPlugin],
  });

  return <DayFlowCalendar calendar={calendar} />;
}

属性参数

属性类型默认值描述
widthnumber | string'240px'侧边栏宽度(如 280 或 '20rem')
miniWidthstring'50px'折叠状态下的侧边栏宽度
initialCollapsedbooleanfalse是否以折叠状态启动侧边栏
createCalendarMode'inline' | 'modal''inline'创建日历表单的显示方式
colorPickerMode'blossom' | 'default''default'使用的颜色选择器组件
onSubscribeCalendar(calendar, events) => Promise<void>undefined成功订阅新日历后的回调
onLoadSubscription(calendar) => Promise<void>undefined针对现有订阅的自定义加载逻辑
onReorder(calendars: CalendarType[]) => void | Promise<void>undefined日历排序变更后的回调
groupsstring[][]恢复已保存的分组名称和顺序
onGroupCreate(groupName: string) => void | Promise<void>undefined创建分组时触发的回调
onGroupRename(previousName, nextName, calendars) => void | Promise<void>undefined重命名分组时触发的回调
onGroupDelete(groupName, calendars) => void | Promise<void>undefined删除分组时触发的回调
onGroupReorder(groups: string[]) => void | Promise<void>undefined分组顺序变化时触发的回调
componentsOrder('calendarList' | 'miniCalendar')[]['calendarList', 'miniCalendar']侧边栏组件显示顺序
render(props: CalendarSidebarRenderProps) => TNodeundefined完全自定义侧边栏 UI
renderSidebarHeader(args: SidebarHeaderSlotArgs) => TNodeundefined自定义侧边栏头部渲染器
renderCalendarContextMenu(calendar, onClose) => TNodeundefined日历项的自定义右键菜单渲染器
renderCreateCalendarDialog(props) => TNodeundefined自定义创建日历对话框渲染器

执行分组变更前会等待对应回调完成。如果回调返回的 Promise 被拒绝,分组及其顺序将保持不变,相关弹窗也会继续显示。calendars 参数包含当前属于该分组的所有日历;onGroupReorder 会收到完整且已排序的分组名称列表。

const sidebarPlugin = createSidebarPlugin({
  groups: savedGroups,
  onGroupCreate: async groupName => saveGroup(groupName),
  onGroupRename: async (previousName, nextName, calendars) =>
    renameGroup(previousName, nextName, calendars),
  onGroupDelete: async (groupName, calendars) =>
    deleteGroup(groupName, calendars),
  onGroupReorder: async groups => saveGroupOrder(groups),
});

编程式 API

日历渲染完成后,通过 app.getPlugin 获取 SidebarService:

import { type SidebarService } from '@dayflow/plugin-sidebar';

const sidebar = app.getPlugin<SidebarService>('sidebar');

sidebar?.collapse(); // 折叠侧边栏
sidebar?.expand(); // 展开侧边栏
sidebar?.setCollapsed(true); // 显式设置状态
console.log(sidebar?.isCollapsed()); // 读取当前状态

SidebarService

方法返回值说明
collapse()void折叠侧边栏
expand()void展开侧边栏
setCollapsed(collapsed: boolean)void显式设置折叠状态
isCollapsed()boolean返回当前的折叠状态

绑定到键盘快捷键

window.addEventListener('keydown', e => {
  if (e.metaKey && e.key === '[') {
    const sidebar = app.getPlugin<SidebarService>('sidebar');
    sidebar?.setCollapsed(!sidebar.isCollapsed());
  }
});

功能

侧边栏插件提供包含颜色点、可见性开关和折叠按钮的侧栏布局。它会自动与日历状态同步,无需额外代码。同时内置了日历管理与右键菜单功能。

内置功能包括:

  • 迷你日历 - 用于快速日期导航的紧凑月视图
  • 日历列表 - 通过彩色复选框切换各个日历的可见性
  • 创建日历 - 使用自定义名称和颜色添加新日历
  • 重命名 / 更改颜色 - 右键点击日历以重命名或更改颜色
  • 日历排序 - 通过在列表中拖拽日历来调整排序。使用 onReorder 回调将新顺序持久化到后端。
  • 日历分组 - 右键侧边栏可新建分组;右键分组可重命名、二次确认后删除,或直接在该分组中创建日历。
  • 分组排序 - 拖动分组标题即可调整顺序。使用 groups 恢复已保存的顺序,并通过 onGroupReorder 持久化变更。
  • 合并日历 - 将一个日历的所有事件移动到另一个日历
  • 删除日历 - 带确认步骤的日历删除(也可先合并)
  • 导入 / 导出 - 导入 .ics 文件或将日历导出为 .ics
  • 订阅日历 - 通过 ICS URL 订阅远程日历,已订阅的日历在侧边栏列表中会显示标识图标

在线示例

这一页用两个示例展示侧边栏的不同形态:一个是开箱即用的默认布局,另一个是使用框架(如 React)完全定制的版本。

可以尝试

  • 可见性切换:在两个示例中切换日历可见性,观察 UI 如何瞬时同步
  • 拖拽同步:拖拽或创建事件,侧栏的"即将到来"卡片会实时更新
  • 收起侧栏:折叠侧栏,看看在周视图等高密度布局里如何回收更多空间

订阅日历

侧边栏内置了订阅远程 ICS 日历源的功能。在侧边栏的日历列表或空白区域右键点击打开菜单,选择订阅日历,然后输入任意公开的 .ics 地址即可。

DayFlow 会自动获取该文件、解析事件并创建一个新日历。已订阅的日历在侧边栏列表中会显示一个小图标,以便与本地创建的日历区分。

自动加载与去重

如果你在初始配置中提供了带有 subscription.url 的日历,侧边栏插件会在挂载时自动获取最新事件。

为了防止将本地缓存的事件与新鲜的订阅数据结合时出现视觉重复,DayFlow 实现了基于 ID 的去重机制:

  • 如果来自订阅的事件与核心存储中已有的事件具有相同的 id,订阅版本的事件将优先显示。
  • 这确保了你的 UI 始终显示最新信息,而不会在页面刷新后出现“双重”事件。

持久化订阅日历

DayFlow 通过 CalendarType 对象上是否存在 subscription 字段来识别订阅日历。

订阅日历的默认行为:

  • 只读:UI 修改操作(编辑标题、更换日历、删除)会通过 canMutateFromUI() 禁用。
  • 不可拖拽:禁用拖放操作(移动、调整大小)。
  • 隐藏备注:如果事件没有描述,详情面板中的“备注”字段将被隐藏,以保持界面整洁。

如果您希望允许用户修改订阅事件或移动它们,可以通过在日历对象上设置 readOnly: false 来显式覆盖这些默认保护:

const calendar = useCalendarApp({
  calendars: [
    {
      id: 'team-ics',
      name: '团队日历',
      subscription: {
        url: 'https://example.com/calendar.ics',
        status: 'ready',
      },
      // 覆盖默认保护:
      readOnly: false, // 同时启用 UI 修改和拖放操作
      colors: {/* ... */},
    },
  ],
});

将日历状态保存到后端时,请保留 subscription 字段,并在下次加载时恢复。

注意: DayFlow 不会自动刷新已订阅 of 日历源。如需保持事件最新,请在应用中自行实现定期拉取逻辑,并通过 app.addEvent() / app.removeEvent() 更新事件。

完全自定义侧边栏

你可以通过传入 render 函数到 createSidebarPlugin (React) 或者使用组件插槽/模板 (Vue, Angular, Svelte) 来完全替换侧边栏。

import { useCalendarApp, DayFlowCalendar } from '@dayflow/react';
import {
  createSidebarPlugin,
  type CalendarSidebarRenderProps,
} from '@dayflow/plugin-sidebar';

const CustomSidebar = ({
  app,
  calendars,
  toggleCalendarVisibility,
  isCollapsed,
  setCollapsed,
}: CalendarSidebarRenderProps) => {
  if (isCollapsed) {
    return (
      <div className='p-2'>
        <button onClick={() => setCollapsed(false)}>展开 →</button>
      </div>
    );
  }

  return (
    <aside className='flex h-full flex-col gap-4 p-4 bg-slate-50 border-r'>
      <header className='flex items-center justify-between'>
        <h3 className='font-semibold'>我的工作区</h3>
        <button onClick={() => setCollapsed(true)}>收起 ←</button>
      </header>

      <nav className='space-y-1'>
        {calendars.map(calendar => (
          <label
            key={calendar.id}
            className='flex items-center gap-2 cursor-pointer'
          >
            <input
              type='checkbox'
              checked={calendar.isVisible}
              onChange={e =>
                toggleCalendarVisibility(calendar.id, e.target.checked)
              }
            />
            <span
              className='w-3 h-3 rounded-full'
              style={{ backgroundColor: calendar.colors.lineColor }}
            />
            {calendar.name}
          </label>
        ))}
      </nav>

      <div className='mt-auto pt-4 border-t text-xs text-slate-500'>
        总事件数:{app.getEvents().length}
      </div>
    </aside>
  );
};

function App() {
  const calendar = useCalendarApp({
    views: [/* 视图 */],
    plugins: [
      createSidebarPlugin({
        render: props => <CustomSidebar {...props} />,
      }),
    ],
  });

  return <DayFlowCalendar calendar={calendar} />;
}

自定义右键菜单

可以替换日历项的默认右键菜单:

createSidebarPlugin({
  renderCalendarContextMenu: (calendar, onClose) => (
    <div className='bg-white shadow-lg border rounded p-2'>
      <button
        onClick={() => {
          console.log('Custom action');
          onClose();
        }}
      >
        Custom Action for {calendar.name}
      </button>
    </div>
  ),
});

自定义创建日历对话框

可以替换默认的创建日历对话框:

createSidebarPlugin({
  renderCreateCalendarDialog: ({ onCreate, onClose }) => (
    <MyCustomDialog onSave={onCreate} onCancel={onClose} />
  ),
});

自定义侧边栏头部

可以替换默认的侧边栏头部(包含“日历”标题和折叠按钮的区域):

createSidebarPlugin({
  renderSidebarHeader: ({ isCollapsed, onCollapseToggle }) => (
    <div className='flex items-center justify-between p-4 border-b'>
      {!isCollapsed && <span className='font-bold text-lg'>My Calendars</span>}
      <button
        onClick={onCollapseToggle}
        className='p-1 hover:bg-slate-100 rounded'
      >
        {isCollapsed ? '→' : '←'}
      </button>
    </div>
  ),
});

本页内容