# Calendar 日历

日历组件，月/年两种模式，支持选中、禁用日期、周号与键盘网格导航；日期描述走 `Intl.DateTimeFormat`（locale 感知）。

## 基础用法

<DemoBlock title="月视图（mode=month）">
  <oas-calendar value="2026-08-09"></oas-calendar>
</DemoBlock>

点击日期选中并派发 `oas-change`；标题可点开月选择面板快速跳月。

触屏（pointer: coarse）下日格最小高度、月格/年格最小高与头部导航钮自动抬到 44px 触控目标（日格只抬高度不撑宽度，格宽随容器均分）。

## 年视图

<DemoBlock title="年视图（mode=year）">
  <oas-calendar mode="year" value="2026-07"></oas-calendar>
</DemoBlock>

年模式选中月份派发 `yyyy-MM`。

## 禁用范围

<DemoBlock title="min / max 限制">
  <oas-calendar value="2026-08-09" min="2026-08-01" max="2026-08-31"></oas-calendar>
</DemoBlock>

## 禁用回调

<DemoBlock title="disabled-date（禁用周末）">
  <oas-calendar id="calendar-disabled-date" value="2026-08-09"></oas-calendar>
</DemoBlock>

`disabledDate` 走 property 传入回调（JSON 无法表达函数）。

## 周号

<DemoBlock title="显示周号（show-week-number）">
  <oas-calendar value="2026-08-09" show-week-number></oas-calendar>
</DemoBlock>

## 事件

<DemoBlock title="oas-change 事件">
  <oas-calendar id="calendar-event" value="2026-08-09"></oas-calendar>
  <span id="calendar-output" style="color: var(--oas-color-text-secondary); font-size: var(--oas-font-size-sm)"></span>
</DemoBlock>

## 自定义单元格

<DemoBlock title="事件标记日历（oas-cell-render 标记节假日）">
  <oas-calendar id="calendar-cell-render" value="2026-08-09"></oas-calendar>
</DemoBlock>

每个日单元格渲染时派发 `oas-cell-render`（`detail: { date, element }`），宿主可在单元格内追加标记/徽标/富文本（如节假日、事件点）；也可在组件内放 `<template slot="cell">` 提供静态骨架，`[data-cell-date]` 节点自动绑定日期数字。`element` 内追加 `<span class="cell-dot">` 即显示内置标记点（`--oas-color-danger` token，明暗主题自适应）。

## 排期条目（events）

<DemoBlock title="events 排期条目（声明式标记 + 当日条目浮层）">
  <oas-calendar id="calendar-events" value="2026-08-01" events='[{"date":"2026-08-08","title":"发布 v2.6","color":"#dc2626"},{"date":"2026-08-08","title":"回归全绿"},{"date":"2026-08-15","title":"评审会"},{"date":"2026-08-22","title":"对齐 review","color":"#059669"},{"date":"2026-08-22","title":"文档批"},{"date":"2026-08-22","title":"定版"}]'></oas-calendar>
</DemoBlock>

`events` 属性/属性通道声明当日条目：`[{ date: 'YYYY-MM-DD', title?, color? }]`。当日有条目时格内渲染圆点（有 `color` 用之、无则主色），多于 2 条合并为 `+N` 徽标；悬停或聚焦当日展开**行内条目浮层**（标题逐行 + 色点）。纯声明式数据通道，不经手写 `oas-cell-render` 监听（该事件仍保留为兜底）。不含跨日条目 / 拖拽改期 / 周视图（属 Scheduler 级别能力）。

## 模式切换

<DemoBlock title="模式切换（month ↔ year 快速跳年）">
  <div style="display:flex;gap:var(--oas-space-1);margin-bottom:var(--oas-space-2)">
    <oas-button id="calendar-mode-month" size="small">月视图</oas-button>
    <oas-button id="calendar-mode-year" size="small">年视图</oas-button>
  </div>
  <oas-calendar id="calendar-mode" value="2026-08-09"></oas-calendar>
  <span id="calendar-mode-output" style="color: var(--oas-color-text-secondary); font-size: var(--oas-font-size-sm)"></span>
</DemoBlock>

`mode="year"` 显示 12 个月网格，标题栏「上一年/下一年」按钮快速切换年份；点击月份选中（派发 `oas-change`，value 为 `yyyy-MM`）并自动切回月视图（派发 `oas-mode-change`）。受控场景监听 `oas-mode-change` 后重新设置 `mode` 属性即可保持指定模式。

## 周起始覆写

<DemoBlock title="周起始覆写（first-day-of-week）">
  <oas-calendar value="2026-08-09" first-day-of-week="0"></oas-calendar>
</DemoBlock>

`first-day-of-week` 取值 `0`（周日）～`6`（周六），缺省随 locale（欧陆/中文周一起始，日/韩/英/阿周日起始；其余走 `Intl.Locale` 周信息）。

## 面板月锚点

<DemoBlock title="page-show-date 锚定面板月 + oas-panel-change">
  <oas-calendar id="calendar-page" value="1980-05-03" page-show-date="2026-08"></oas-calendar>
  <span id="calendar-page-output" style="color: var(--oas-color-text-secondary); font-size: var(--oas-font-size-sm)"></span>
</DemoBlock>

`page-show-date` 把面板初始/受控锚定到指定月份——即使 `value` 在别的年份（如回显出生日期时希望从当月打开）；其优先级高于 value。用户翻页/选月/跳月时派发 `oas-panel-change`（`detail: { date }`）；移除属性后面板回到 value 所在月。

## 边界翻页

<DemoBlock title="min / max 翻页边界">
  <oas-calendar value="2026-06-15" min="2026-06-01" max="2026-09-30"></oas-calendar>
</DemoBlock>

翻页会跨出 `[min, max]` 的整月/整年（月面板为整年、十年面板为整十年页）时，「上一月/下一月」按钮自动置灰。

## 范围选择

<DemoBlock title="range：起止两点选取 + 区间高亮">
  <div style="width: 100%">
    <oas-calendar id="calendar-range" range page-show-date="2026-08-01"></oas-calendar>
    <p style="width: 100%; color: var(--oas-color-text-secondary); font-size: var(--oas-font-size-sm); margin: 0">
      已选区间：<span id="calendar-range-value">点击起点，再点终点（悬停有预览）</span>
    </p>
  </div>
</DemoBlock>

设置 `range` 进入范围选择模式：第一次点击选定起点（起点高亮），移动指针时悬停日格实时预览区间，第二次点击选定终点并派发 `oas-change`（`detail: { start, end }`，均为 `YYYY-MM-DD` 字符串；终点早于起点时自动交换）。起止端点主色实底圆角、中间段主色浅底，颜色全部走 token（与 date-picker daterange 同一视觉语言）。提交后再次点击即重开新一轮（旧区间即时清除）。`value` 属性以 JSON 数组回显/预置范围：`value='["2026-08-05","2026-08-15"]'`。

## 范围 / 多选与选周：请用 date-picker

本组件（常驻选择面板）单选日期开箱即用；范围选择已由上方 `range` 属性覆盖。多选日期、按周选择仍由 [date-picker](./date-picker) 覆盖——二者共享同一日期网格，语义一致：

- **多选日期**：`<oas-date-picker multiple>`
- **按周选择**：`<oas-date-picker type="week">`（值 `yyyy-Wnn`）

若需要常驻页面里做多选，宿主可把 `oas-date-picker` 的内层面板相关能力与 `oas-calendar` 组合，或用 `oas-calendar` 单选 + 宿主自己维护集合。

## 自定义头部

<DemoBlock title="header 插槽（自定义头部，缺省内置导航）">
  <oas-calendar id="calendar-header-slot" value="2026-08-09">
    <div slot="header" style="display:flex;align-items:center;justify-content:space-between;gap:var(--oas-space-2);min-height:var(--oas-control-height-md)">
      <strong>排期月历</strong>
      <oas-button size="small" id="calendar-header-slot-btn">回到今天</oas-button>
    </div>
  </oas-calendar>
  <span id="calendar-header-slot-output" style="color: var(--oas-color-text-secondary); font-size: var(--oas-font-size-sm)"></span>
</DemoBlock>

`slot="header"` 分发自定义头部（整体替换内置导航区，宿主自行接管翻页交互——可配合 `page-show-date` 受控锚定面板月）；不分发时保持内置 `prev / title / next / today` 导航（`[part="header"]` 可定位、`::part` 改样式）。

## 面板语言与头部格式

<DemoBlock title="locale 面板语言覆盖">
  <oas-calendar id="calendar-locale" value="2026-08-09" locale="en"></oas-calendar>
</DemoBlock>

`locale` 覆盖面板语言：标题/周头/单元格描述等 Intl 格式化、导航与「今天」文案、周起始推导一并跟随；缺省跟随 config-provider 注入或全局 locale。

<DemoBlock title="format 头部日期格式串">
  <oas-calendar id="calendar-format" value="2026-08-09" format="yyyy年MM月"></oas-calendar>
</DemoBlock>

`format` 自定义头部标题格式（token 同 date-picker：`yyyy`/`MM`/`dd`/`HH`/`mm`/`ss`，日面板与月面板标题均适用）；十年面板标题为区间形态（`2020-2031`），格式串不适用。

## 自定义头部操作条：卡片组合（额外内容，不替换内置导航）

要在日历上方放品牌/操作条且**保留内置导航**，把 `oas-calendar` 包进宿主自己的卡片，操作条放在顶部即可（需要整体替换头部请用上方 `header` 插槽）：

<DemoBlock title="header 组合：自定义头部操作条">
  <div style="display: inline-flex; flex-direction: column; gap: var(--oas-space-2); padding: var(--oas-space-3); border: 1px solid var(--oas-color-border); border-radius: var(--oas-radius-lg);">
    <div style="display: inline-flex; justify-content: space-between; align-items: center; width: 100%; gap: var(--oas-space-2);">
      <span style="font-weight: 600;">项目排期</span>
      <span style="font-size: var(--oas-font-size-xs); color: var(--oas-color-text-secondary)">点标题可钻取年/月，内置「今天」钮回当月</span>
    </div>
    <oas-calendar id="calendar-header-composite" value="2026-08-09"></oas-calendar>
  </div>
</DemoBlock>

## 只读日历

<DemoBlock title="readonly：详情页只读 + 事件点">
  <oas-calendar id="calendar-readonly" value="2026-08-09" readonly></oas-calendar>
</DemoBlock>

`readonly` 下可翻页浏览、可进入月/十年面板钻取，但点选与键盘 `Enter` 不提交——配合 `oas-cell-render` 标记事件点，即「可看不可改」的详情日历。

## 全局禁用

<DemoBlock title="disabled：整体不可交互">
  <oas-calendar value="2026-08-09" disabled></oas-calendar>
</DemoBlock>

`disabled` 整体置灰并停止全部交互（点选/翻页/键盘），配合表单禁用场景；`readonly` 仍允许浏览。

## 键盘与快速跳年

<DemoBlock title="扩展键盘 + decade 快速跳远年">
  <oas-calendar value="2026-08-09"></oas-calendar>
</DemoBlock>

- 键盘：`Home`/`End` 跳周首/周尾；`PageUp`/`PageDown` 翻上/下一月，`Shift` 同键翻年；方向键逐格移动，`Enter`/`Space` 选中。
- 快速跳远年：点标题进入月面板，再点年份进入十年网格（按 ±12 年翻页），选年回到该年的月面板、选月回到日视图——从 2026 跳到 1980 约需 7-8 次点击。

## 非公历历法（calendar-system）

<DemoBlock title="农历（calendar-system=chinese）">
  <oas-calendar id="calendar-system-cn" value="2026-08-09" calendar-system="chinese"></oas-calendar>
</DemoBlock>

<DemoBlock title="伊斯兰历（calendar-system=islamic）">
  <oas-calendar id="calendar-system-islamic" value="2026-08-09" calendar-system="islamic"></oas-calendar>
</DemoBlock>

`calendar-system` 透传 `Intl.DateTimeFormat` 的 `calendar` 选项（`chinese` / `islamic` / `hebrew` 等）：标题、周头、单元格数字与 aria 描述均跟随历法；日期计算与 `value` / `data-date` 仍走公历内部模型——历法只影响展示与读屏，选中/提交语义不变。

## 多月份面板（months）

<DemoBlock title="months=2：两个月并排 + 翻页联动">
  <oas-calendar id="calendar-months" value="2026-08-09" months="2"></oas-calendar>
</DemoBlock>

`months` 设置并排月份面板数（默认 1，正整数，非法/小于 1 回落 1，超出上限收敛 12）：面板内网格横向排开、各占等宽，翻页（上一月/下一月）按整段联动（一次步进 `months` 个月），标题展示月份范围。多面板与 `first-day-of-week`、`show-week-number`、`range` 等属性可组合使用。

## API

### oas-calendar

#### 属性

| 属性 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| `calendar-system` | 非公历历法（Intl calendar 选项透传，如 chinese/islamic/hebrew）：标题/周头/日格数字/aria 跟随；日期计算与 value/data-date 仍为公历 | `string` | — |
| `disabled` | 整体禁用：置灰并停止全部交互（点选/翻页/键盘） | `boolean` | — |
| `events` | 排期条目声明（JSON）：[{ date: YYYY-MM-DD, title?, color? }]——当日渲染圆点（有 color 用之，无则主色），>2 合并 +N 徽标；悬停/聚焦当日展开行内条目浮层 | `CalendarEvent[]` | `[]` |
| `first-day-of-week` | 周起始覆写：`0`（周日）～`6`（周六）；缺省随 locale（欧陆/中文周一、日/韩/英/阿周日） | `string` | — |
| `format` | 头部标题格式串（yyyy/MM/dd token，日/月面板适用） | `string` | — |
| `locale` | 面板语言覆盖（标题/周头/单元格描述/导航文案/周起始），缺省走 config-provider > 全局 | `string` | — |
| `max` | 可选范围（ISO 日期）；翻页到整月越界时导航钮自动置灰 | `string` | — |
| `min` | 可选范围（ISO 日期）；翻页到整月越界时导航钮自动置灰 | `string` | — |
| `mode` | `month` / `year`（年模式选中月份后自动切回月视图） | `string` | `month` |
| `months` | 并排月份面板数（1-12，默认 1）：面板横向排开、翻页按整段联动，标题为月份范围 | `string` | — |
| `page-show-date` | 面板月锚点（ISO `yyyy-MM` 或 `yyyy-MM-dd`）：初始/受控锚定显示的月份，优先级高于 value；移除后回到 value 所在月 | `string` | — |
| `range` | 范围选择模式：两段式选取 + 悬停预览 + 区间高亮，oas-change `detail: { start, end }`，value 以 JSON 数组回显 | `boolean` | — |
| `readonly` | 只读：可翻页浏览/钻取面板，点选与键盘 Enter 不提交 | `boolean` | — |
| `show-week-number` | 显示 ISO 周号列 | `boolean` | — |
| `value` | 选中值（ISO） | `string` | — |

#### Property（JS property；是否写回 attribute 见各行说明）

| Property | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| `disabledDate` | 禁用日期谓词（property 函数通道，`(date) => boolean`） | `((d: Date) => boolean) \| null` | — |
| `events` | 排期条目（`CalendarEvent[]`，`{ date, title?, color? }`）：`el.events` 读当前生效条目；`el.events = [...]` 程序性写（同步 events 属性并重渲染，不派发事件） | `CalendarEvent[]` | `[]` |

#### 事件

| 事件 | 说明 |
| --- | --- |
| `oas-cell-render` | 每个日单元格渲染时派发，`detail: { date, element }`（element 为日按钮，宿主可追加标记/徽标/富文本） |
| `oas-change` | 选中变化，`detail: { value }` |
| `oas-mode-change` | 年模式选中月份自动切回月视图时派发，`detail: { mode }` |
| `oas-panel-change` | 用户翻页/选月/跳月使面板所在月份变化时派发，`detail: { date }`（date 为变化后面板首日） |

#### 插槽

| 名称 | 说明 |
| --- | --- |
| `header` | 自定义头部，整体替换内置导航（不分发保持内置 prev/title/next/today） |
| `template[slot="cell"]` | 日单元格静态模板，克隆到每个日按钮；`[data-cell-date]` 节点自动绑定日期数字 |

键盘：`↑`/`↓`/`←`/`→` 在网格内移动（跨月自动翻页），`Home`/`End` 跳周首/周尾，`PageUp`/`PageDown` 翻上/下一月（`Shift` 翻年），`Enter`/`Space` 选中。

<script setup>
import { onMounted } from 'vue'
import { loadLocale } from '@oas-ui/i18n'
onMounted(() => {
  // locale 覆盖 demo 需要目标语言包已注册（按需加载 en 包）
  loadLocale('en')
  const el = document.getElementById('calendar-disabled-date')
  el.disabledDate = (d) => d.getDay() === 0 || d.getDay() === 6

  // range 范围选择 demo：显示选中区间
  const rangeCal = document.getElementById('calendar-range')
  rangeCal?.addEventListener('oas-change', (e) => {
    document.getElementById('calendar-range-value').textContent = `${e.detail.start} ~ ${e.detail.end}`
  })
  const ev = document.getElementById('calendar-event')
  const out = document.getElementById('calendar-output')
  ev?.addEventListener('oas-change', (e) => {
    out.textContent = `oas-change: ${e.detail.value}`
  })

  // 自定义单元格：标记节假日（建军节 8-01、8-15）
  const cell = document.getElementById('calendar-cell-render')
  cell?.addEventListener('oas-cell-render', (e) => {
    const { date, element } = e.detail
    const holiday = date.getMonth() === 7 && (date.getDate() === 1 || date.getDate() === 15)
    element.classList.toggle('holiday', holiday)
    if (holiday && !element.querySelector('.cell-dot')) {
      const dot = document.createElement('span')
      dot.className = 'cell-dot'
      dot.setAttribute('role', 'img')
      dot.setAttribute('aria-label', '节日')
      element.appendChild(dot)
    }
  })

  // 面板月锚点：显示当前面板（标题读 shadow 内节点），翻页/选月后同步
  const pageCal = document.getElementById('calendar-page')
  const pageOut = document.getElementById('calendar-page-output')
  const syncPage = () => {
    const t = pageCal?.shadowRoot?.querySelector('[part="title"]')?.textContent ?? ''
    if (pageOut) pageOut.textContent = `当前面板：${t}`
  }
  pageCal?.addEventListener('oas-panel-change', (e) => {
    syncPage()
    pageOut.textContent = `${pageOut.textContent}（oas-panel-change: ${e.detail.date}）`
  })
  syncPage()

  // 只读详情日历：标记纪念日（8-08、8-18）
  const ro = document.getElementById('calendar-readonly')
  ro?.addEventListener('oas-cell-render', (e) => {
    const { date, element } = e.detail
    const mark = date.getMonth() === 7 && (date.getDate() === 8 || date.getDate() === 18)
    if (mark && !element.querySelector('.cell-dot')) {
      const dot = document.createElement('span')
      dot.className = 'cell-dot'
      dot.setAttribute('role', 'img')
      dot.setAttribute('aria-label', '纪念日')
      element.appendChild(dot)
    }
  })

  // 模式切换：month ↔ year，显示当前模式与选中反馈
  // 年模式选月切回月视图时 oas-mode-change 与 oas-change 同帧先后派发，
  // 反馈文本追加写入（覆盖会吞掉前一个事件，用户看不到完整事件序列）
  const modeCal = document.getElementById('calendar-mode')
  const modeOut = document.getElementById('calendar-mode-output')
  const setMode = (m) => modeCal?.setAttribute('mode', m)
  const appendOut = (text) => {
    modeOut.textContent = `${modeOut.textContent}${modeOut.textContent ? ' · ' : ''}${text}`
  }
  document.getElementById('calendar-mode-month')?.addEventListener('click', () => setMode('month'))
  document.getElementById('calendar-mode-year')?.addEventListener('click', () => setMode('year'))
  modeCal?.addEventListener('oas-mode-change', (e) => {
    appendOut(`oas-mode-change: ${e.detail.mode}`)
  })
  modeCal?.addEventListener('oas-change', (e) => {
    appendOut(`oas-change: ${e.detail.value}`)
  })

  // header 插槽 demo：宿主内容可交互（回到今天 = 设 page-show-date 为当月）
  const hs = document.getElementById('calendar-header-slot')
  const hsOut = document.getElementById('calendar-header-slot-output')
  document.getElementById('calendar-header-slot-btn')?.addEventListener('click', () => {
    const n = new Date()
    const ym = `${n.getFullYear()}-${String(n.getMonth() + 1).padStart(2, '0')}`
    hs?.setAttribute('page-show-date', ym)
    if (hsOut) hsOut.textContent = `已锚定回当月：${ym}`
  })
})
</script>
