移动端导航日期选择器

日期选择器DatePicker

从底部弹出的年 / 月 / 日选择器,用于生日、有效期这类需要日期输入的场景

数据录入 规格 18 · 日期选择器 DatePicker 6 端实现 触摸优先

引入(H5 原生;其余 5 端见「快速开始」)
<!-- ① 令牌:PC 令牌 + 移动端 --kole-m-* 合成单文件,引一次 -->
<link rel="stylesheet" href="kole-ui/mobile/tokens.css">

<!-- ② 本组件样式(全量则用 kole-ui/mobile/components/index.css) -->
<link rel="stylesheet" href="kole-ui/mobile/components/mobile-datepicker.css">

<!-- ③ 结构照抄下方任一演示块(类名与 6 端实现一致) -->

演示

每个演示都是真实渲染:预览帧加载 frameworks-mobile/DatePicker.html?demo=<id>(只显示该演示块),代码是该演示块在演示页里的原文,可复制。全部演示同屏可看 演示页 ↗。

01 组件类型

年月日mode=date

三列滚动,选中项用品牌色高亮并以 aria-selected 标注,不依赖视觉位置。

查看代码(演示页原文 · 35 行)
frameworks-mobile/DatePicker.html · date
<section class="demo-block" data-demo="date">
  <p class="demo-label">年月日(mode=date:三列滚动,选中项品牌色高亮;点取消打标记)</p>
  <div class="demo-frame" data-assert="datepicker-date">
    <div class="demo-frame__body">
      <p>已选日期:<span class="demo-value">2026-09-20</span></p>
    </div>
    <div class="kole-m-datepicker__mask is-open" aria-hidden="true"></div>
    <div class="kole-m-datepicker kole-m-datepicker--round is-open" id="dp-date"
         role="dialog" aria-modal="true" aria-labelledby="dp-date-title">
      <div class="kole-m-datepicker__header">
        <button class="kole-m-datepicker__btn" type="button" id="dp-cancel"
                data-behavior="click-sets-attr:#dp-date|data-cancelled|true">取消</button>
        <span class="kole-m-datepicker__title" id="dp-date-title">选择日期</span>
        <button class="kole-m-datepicker__btn kole-m-datepicker__btn--confirm" type="button">确定</button>
      </div>
      <div class="kole-m-datepicker__columns">
        <ul class="kole-m-datepicker__column" role="listbox" aria-label="年">
          <li class="kole-m-datepicker__option" role="option" aria-selected="false">2025</li>
          <li class="kole-m-datepicker__option" role="option" aria-selected="false">2026</li>
          <li class="kole-m-datepicker__option is-selected" role="option" aria-selected="true">2027</li>
        </ul>
        <ul class="kole-m-datepicker__column" role="listbox" aria-label="月">
          <li class="kole-m-datepicker__option" role="option" aria-selected="false">8 月</li>
          <li class="kole-m-datepicker__option is-selected" role="option" aria-selected="true">9 月</li>
          <li class="kole-m-datepicker__option" role="option" aria-selected="false">10 月</li>
        </ul>
        <ul class="kole-m-datepicker__column" role="listbox" aria-label="日">
          <li class="kole-m-datepicker__option" role="option" aria-selected="false">19</li>
          <li class="kole-m-datepicker__option is-selected" role="option" aria-selected="true">20</li>
          <li class="kole-m-datepicker__option" role="option" aria-selected="false">21</li>
        </ul>
      </div>
    </div>
  </div>
</section>
年月mode=month

只有两列,用于账单月份这类不需要具体日期的场景。

查看代码(演示页原文 · 26 行)
frameworks-mobile/DatePicker.html · month
<section class="demo-block" data-demo="month">
  <p class="demo-label">年月(mode=month:只有两列,用于账单月份这类场景)</p>
  <div class="demo-frame" data-assert="datepicker-month">
    <div class="kole-m-datepicker__mask is-open" aria-hidden="true"></div>
    <div class="kole-m-datepicker kole-m-datepicker--round is-open"
         role="dialog" aria-modal="true" aria-labelledby="dp-month-title">
      <div class="kole-m-datepicker__header">
        <button class="kole-m-datepicker__btn" type="button">取消</button>
        <span class="kole-m-datepicker__title" id="dp-month-title">选择月份</span>
        <button class="kole-m-datepicker__btn kole-m-datepicker__btn--confirm" type="button">确定</button>
      </div>
      <div class="kole-m-datepicker__columns">
        <ul class="kole-m-datepicker__column" role="listbox" aria-label="年">
          <li class="kole-m-datepicker__option" role="option" aria-selected="false">2024</li>
          <li class="kole-m-datepicker__option" role="option" aria-selected="false">2025</li>
          <li class="kole-m-datepicker__option is-selected" role="option" aria-selected="true">2026</li>
        </ul>
        <ul class="kole-m-datepicker__column" role="listbox" aria-label="月">
          <li class="kole-m-datepicker__option" role="option" aria-selected="false">8 月</li>
          <li class="kole-m-datepicker__option is-selected" role="option" aria-selected="true">9 月</li>
          <li class="kole-m-datepicker__option" role="option" aria-selected="false">10 月</li>
        </ul>
      </div>
    </div>
  </div>
</section>

02 组件状态

收起态状态 closed

默认态:遮罩 pointer-events: none,页面可正常滚动与点击。

查看代码(演示页原文 · 29 行)
frameworks-mobile/DatePicker.html · closed
<section class="demo-block" data-demo="closed">
  <p class="demo-label">收起态(status=closed:默认态,遮罩与浮层都不可见、不可点)</p>
  <div class="demo-frame" data-assert="datepicker-closed">
    <div class="demo-frame__body">
      <p>浮层收起时页面可正常滚动与点击(遮罩 pointer-events: none)。</p>
      <button class="kole-m-datepicker__trigger" type="button">选择日期</button>
    </div>
    <div class="kole-m-datepicker__mask" aria-hidden="true"></div>
    <div class="kole-m-datepicker kole-m-datepicker--round"
         role="dialog" aria-modal="true" aria-labelledby="dp-closed-title" aria-hidden="true">
      <div class="kole-m-datepicker__header">
        <button class="kole-m-datepicker__btn" type="button">取消</button>
        <span class="kole-m-datepicker__title" id="dp-closed-title">选择日期</span>
        <button class="kole-m-datepicker__btn kole-m-datepicker__btn--confirm" type="button">确定</button>
      </div>
      <div class="kole-m-datepicker__columns">
        <ul class="kole-m-datepicker__column" role="listbox" aria-label="年">
          <li class="kole-m-datepicker__option is-selected" role="option" aria-selected="true">2026</li>
        </ul>
        <ul class="kole-m-datepicker__column" role="listbox" aria-label="月">
          <li class="kole-m-datepicker__option is-selected" role="option" aria-selected="true">9 月</li>
        </ul>
        <ul class="kole-m-datepicker__column" role="listbox" aria-label="日">
          <li class="kole-m-datepicker__option is-selected" role="option" aria-selected="true">20</li>
        </ul>
      </div>
    </div>
  </div>
</section>

API

props / events / slots 为 6 端实现的公共接口(说明文字取自规格对应小节)。字段名与各端源码逐名核对:node tools/verify-mobile-docs.mjs

Props

名称类型默认值说明必传
mode'date' | 'month''date'变体 mode:date 为年月日三列,month 为年月两列(规格 §18.3)N
roundbooleanfalse变体 round:靠内容一侧是否切圆角(规格 §18.3)N
openbooleanfalse状态 open:浮层是否展开(规格 §18.4)N
closeOnMaskbooleantrue遮罩点击是否关闭(规格 §18.5)N
valuestring''当前选中值,YYYY-MM-DD(mode=month 时 YYYY-MM);用于让选中项可见(规格 §18.6)N
yearsArray<string>[]年份列选项(宿主提供,本组件不发明年份范围)(规格 §18.7)N
monthsArray<string>[]月份列选项(规格 §18.2 picker__column)N
daysArray<string>[]日期列选项;mode=month 时不渲染该列(规格 §18.3)N

「必传」按严格定义:实现里**没有默认值**时才为 Y(本门禁逐条核对 props 与各端源码的默认值,防止契约与实现脱节)。

事件

名称参数说明
confirm—点击确定时触发(规格 §18.2 picker__header)
close—点击取消或遮罩(closeOnMask=true 时)时触发(规格 §18.5)

插槽

名称说明
default自定义内容(追加在列区之后)

CSS 变量

组件级变量(在组件样式表里定义)。业务侧可在自己的作用域内覆盖,不必改组件源码。

名称默认值说明
本组件没有组件级 CSS 变量

何时使用

  • 从底部弹出的年 / 月 / 日选择器,用于生日、有效期这类需要日期输入的场景
  • 遮罩点击关闭;closeOnMask=false 时不关闭
  • 选项行高不小于 44px,滚动容器 -webkit-overflow-scrolling: touch
  • 确定 / 取消按钮热区不小于 44px
  • 浮层 role="dialog" + aria-modal="true"
  • 每列 role="listbox",选项 role="option" + aria-selected
  • 遮罩 aria-hidden="true"
  • 选中值以 YYYY-MM-DD 文本呈现(不依赖视觉滚动位置)

交互与触控

  • 遮罩点击关闭;closeOnMask=false 时不关闭
  • 选项行高不小于 44px,滚动容器 -webkit-overflow-scrolling: touch
  • 确定 / 取消按钮热区不小于 44px

无障碍

  • 浮层 role="dialog" + aria-modal="true"
  • 每列 role="listbox",选项 role="option" + aria-selected
  • 遮罩 aria-hidden="true"
  • 选中值以 YYYY-MM-DD 文本呈现(不依赖视觉滚动位置)

从「该用哪一个」的角度区分;PC 端的对应实现见 PC 文档站。

组件何时用它而不是本组件
动作面板ActionSheet从底部弹出的选择器与动作面板形态接近;选项是日期时才用日期选择器
单元格Cell「已选日期」的展示入口用单元格,点开才拉起选择器
弹出层Popup日期选择器本身就是底部浮层,不要在弹出层里再套一层

规格未定 / 禁止发明

类别条目
禁止发明日期范围的禁用规则(由宿主传入)
禁止发明滚轮惯性 / 吸附动画的物理参数
规格未定可选年份的范围
规格未定是否支持「至今」这类特殊选项

结构(anatomy)

字段说明
mask遮罩,点击关闭
picker底部浮层
picker__header取消 / 标题 / 确定
picker__columns年 / 月 / 日三列
picker__column单列,可滚动
picker__option单个选项

变体维度与类名映射

类名映射由构建脚本从契约 variantClasses 生成,并被 verify:mobile-docs 逐条对照组件 CSS 校验(类/变量必须真实存在)。

维度取值对应类名 / 变量
modedate / month
date (由数据驱动,无专属类)
month (由数据驱动,无专属类)
roundfalse / true
false (由数据驱动,无专属类)
true .kole-m-datepicker--round

代表变体

变体标签
mode=date · round=true年月日三列
mode=month · round=true年月两列
mode=date · round=false直角(不含圆角)

用到的令牌

构建时从本组件样式表扫描得出。蓝色为移动端自有令牌,绿色为继承的 PC 令牌(改一处两端生效)。

--kole-m-duration-slide --kole-m-ease-slide --kole-m-font-size-body --kole-m-font-size-label --kole-m-gutter --kole-m-safe-bottom --kole-m-touch-target --kole-color-border --kole-color-brand --kole-color-brand-bg --kole-color-card-bg --kole-color-focus-ring --kole-color-mask --kole-color-text-body --kole-color-text-title --kole-font-family --kole-radius-base --kole-radius-large --kole-space-4 --kole-space-8

6 端源码

同一组件的六份实现(生产环境的类名与结构一致,差异只在技术栈写法与单位)。点开查看,右侧可复制。

frameworks-mobile/DatePicker.css · 纯样式(CSS) · 145 行
frameworks-mobile/DatePicker.css
/* Kole UI Mobile · DatePicker 样式 — 对齐移动端规格 §18
   日期选择器:底部浮层 + 三列滚动;选项行高 ≥44px;选中项品牌色高亮。
   日期值的真源是 aria-selected 与文本(不依赖视觉滚动位置)。 */

.kole-m-datepicker__mask {
  position: fixed;
  inset: 0;
  z-index: 2000;
  background: var(--kole-color-mask);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--kole-m-duration-slide) var(--kole-m-ease-slide);
}

.kole-m-datepicker__mask.is-open {
  opacity: 1;
  pointer-events: auto;
}

.kole-m-datepicker {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 2001;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  padding-bottom: var(--kole-m-safe-bottom);
  background: var(--kole-color-card-bg);
  font-family: var(--kole-font-family);
  transform: translateY(100%);
  transition: transform var(--kole-m-duration-slide) var(--kole-m-ease-slide);
}

.kole-m-datepicker.is-open { transform: translateY(0); }

/* 变体 round=true:靠内容一侧切圆角 */
.kole-m-datepicker--round {
  border-start-start-radius: var(--kole-radius-large);
  border-start-end-radius: var(--kole-radius-large);
}

.kole-m-datepicker__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--kole-space-8);
  padding: var(--kole-space-8) var(--kole-m-gutter);
  border-bottom: 1px solid var(--kole-color-border);
  font-size: var(--kole-m-font-size-body);
  font-weight: 500;
}

.kole-m-datepicker__title {
  color: var(--kole-color-text-title);
  font-weight: 500;
}

.kole-m-datepicker__btn {
  box-sizing: border-box;
  min-height: var(--kole-m-touch-target);
  padding: 0 var(--kole-space-8);
  border: 0;
  border-radius: var(--kole-radius-base);
  background: none;
  color: var(--kole-color-brand);
  font-family: inherit;
  font-size: var(--kole-m-font-size-label);
  line-height: 1;
  cursor: pointer;
  touch-action: manipulation;
}

.kole-m-datepicker__btn:focus-visible {
  outline: 2px solid var(--kole-color-focus-ring);
  outline-offset: 2px;
}

.kole-m-datepicker__btn:active { background: var(--kole-color-brand-bg); }

.kole-m-datepicker__btn--confirm { font-weight: 500; }

.kole-m-datepicker__columns {
  display: flex;
  align-items: stretch;
  gap: 1px;
  padding: 0 0 var(--kole-space-8);
  background: var(--kole-color-card-bg);
}

.kole-m-datepicker__column {
  flex: 1 1 0;
  min-width: 0;
  max-height: 200px;
  margin: 0;
  padding: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  list-style: none;
}

.kole-m-datepicker__option {
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: var(--kole-m-touch-target);
  padding: 0 var(--kole-space-4);
  color: var(--kole-color-text-body);
  font-size: var(--kole-m-font-size-label);
  line-height: 1.3;
  text-align: center;
}

/* 状态 selected:当前选中项(品牌色高亮) */
.kole-m-datepicker__option.is-selected {
  color: var(--kole-color-brand);
  font-weight: 500;
}

.kole-m-datepicker__option[aria-selected="true"] {
  color: var(--kole-color-brand);
  font-weight: 500;
}

/* 演示页触发器(非组件交付样式) */
.kole-m-datepicker__trigger {
  min-height: var(--kole-m-touch-target);
  padding: 0 var(--kole-m-gutter);
  border: 1px solid var(--kole-color-border);
  border-radius: var(--kole-radius-base);
  background: var(--kole-color-card-bg);
  color: var(--kole-color-text-body);
  font-family: inherit;
  font-size: var(--kole-m-font-size-label);
  cursor: pointer;
  touch-action: manipulation;
}

.kole-m-datepicker__trigger:focus-visible {
  outline: 2px solid var(--kole-color-focus-ring);
  outline-offset: 2px;
}
frameworks-mobile/DatePicker.html · H5 原生(无框架) · 152 行
frameworks-mobile/DatePicker.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>Kole UI Mobile · DatePicker(H5)</title>
<link rel="stylesheet" href="../.design_library/kole-ui-mobile/colors_and_type.css">
<link rel="stylesheet" href="DatePicker.css">
<style>
  body { margin: 0; background: var(--kole-color-page-bg); font-family: var(--kole-font-family); color: var(--kole-color-text-body); }
  .demo { max-width: 375px; margin: 0 auto; padding: var(--kole-m-gutter) 0; }
  .demo-label { margin: 0; padding: var(--kole-space-12) var(--kole-m-gutter) var(--kole-space-8);
    font-size: var(--kole-m-font-size-label); color: var(--kole-color-text-secondary); }
  /* 展示框:transform 建立包含块,遮罩与浮层限定在框内(生产环境落在视口上) */
  .demo-frame { position: relative; max-width: 375px; margin: 0 auto; height: 300px;
    overflow: hidden; transform: translateZ(0); background: var(--kole-color-page-bg);
    border-block: 1px solid var(--kole-color-border); }
  .demo-frame__body { padding: var(--kole-m-gutter); font-size: var(--kole-m-font-size-label);
    color: var(--kole-color-text-secondary); line-height: 1.7; }
  .demo-value { color: var(--kole-color-text-body); font-weight: 500; }
  .demo-block[hidden] { display: none; }
</style>
</head>
<body>
<div class="demo">
  <section class="demo-block" data-demo="date">
    <p class="demo-label">年月日(mode=date:三列滚动,选中项品牌色高亮;点取消打标记)</p>
    <div class="demo-frame" data-assert="datepicker-date">
      <div class="demo-frame__body">
        <p>已选日期:<span class="demo-value">2026-09-20</span></p>
      </div>
      <div class="kole-m-datepicker__mask is-open" aria-hidden="true"></div>
      <div class="kole-m-datepicker kole-m-datepicker--round is-open" id="dp-date"
           role="dialog" aria-modal="true" aria-labelledby="dp-date-title">
        <div class="kole-m-datepicker__header">
          <button class="kole-m-datepicker__btn" type="button" id="dp-cancel"
                  data-behavior="click-sets-attr:#dp-date|data-cancelled|true">取消</button>
          <span class="kole-m-datepicker__title" id="dp-date-title">选择日期</span>
          <button class="kole-m-datepicker__btn kole-m-datepicker__btn--confirm" type="button">确定</button>
        </div>
        <div class="kole-m-datepicker__columns">
          <ul class="kole-m-datepicker__column" role="listbox" aria-label="年">
            <li class="kole-m-datepicker__option" role="option" aria-selected="false">2025</li>
            <li class="kole-m-datepicker__option" role="option" aria-selected="false">2026</li>
            <li class="kole-m-datepicker__option is-selected" role="option" aria-selected="true">2027</li>
          </ul>
          <ul class="kole-m-datepicker__column" role="listbox" aria-label="月">
            <li class="kole-m-datepicker__option" role="option" aria-selected="false">8 月</li>
            <li class="kole-m-datepicker__option is-selected" role="option" aria-selected="true">9 月</li>
            <li class="kole-m-datepicker__option" role="option" aria-selected="false">10 月</li>
          </ul>
          <ul class="kole-m-datepicker__column" role="listbox" aria-label="日">
            <li class="kole-m-datepicker__option" role="option" aria-selected="false">19</li>
            <li class="kole-m-datepicker__option is-selected" role="option" aria-selected="true">20</li>
            <li class="kole-m-datepicker__option" role="option" aria-selected="false">21</li>
          </ul>
        </div>
      </div>
    </div>
  </section>

  <section class="demo-block" data-demo="month">
    <p class="demo-label">年月(mode=month:只有两列,用于账单月份这类场景)</p>
    <div class="demo-frame" data-assert="datepicker-month">
      <div class="kole-m-datepicker__mask is-open" aria-hidden="true"></div>
      <div class="kole-m-datepicker kole-m-datepicker--round is-open"
           role="dialog" aria-modal="true" aria-labelledby="dp-month-title">
        <div class="kole-m-datepicker__header">
          <button class="kole-m-datepicker__btn" type="button">取消</button>
          <span class="kole-m-datepicker__title" id="dp-month-title">选择月份</span>
          <button class="kole-m-datepicker__btn kole-m-datepicker__btn--confirm" type="button">确定</button>
        </div>
        <div class="kole-m-datepicker__columns">
          <ul class="kole-m-datepicker__column" role="listbox" aria-label="年">
            <li class="kole-m-datepicker__option" role="option" aria-selected="false">2024</li>
            <li class="kole-m-datepicker__option" role="option" aria-selected="false">2025</li>
            <li class="kole-m-datepicker__option is-selected" role="option" aria-selected="true">2026</li>
          </ul>
          <ul class="kole-m-datepicker__column" role="listbox" aria-label="月">
            <li class="kole-m-datepicker__option" role="option" aria-selected="false">8 月</li>
            <li class="kole-m-datepicker__option is-selected" role="option" aria-selected="true">9 月</li>
            <li class="kole-m-datepicker__option" role="option" aria-selected="false">10 月</li>
          </ul>
        </div>
      </div>
    </div>
  </section>

  <section class="demo-block" data-demo="closed">
    <p class="demo-label">收起态(status=closed:默认态,遮罩与浮层都不可见、不可点)</p>
    <div class="demo-frame" data-assert="datepicker-closed">
      <div class="demo-frame__body">
        <p>浮层收起时页面可正常滚动与点击(遮罩 pointer-events: none)。</p>
        <button class="kole-m-datepicker__trigger" type="button">选择日期</button>
      </div>
      <div class="kole-m-datepicker__mask" aria-hidden="true"></div>
      <div class="kole-m-datepicker kole-m-datepicker--round"
           role="dialog" aria-modal="true" aria-labelledby="dp-closed-title" aria-hidden="true">
        <div class="kole-m-datepicker__header">
          <button class="kole-m-datepicker__btn" type="button">取消</button>
          <span class="kole-m-datepicker__title" id="dp-closed-title">选择日期</span>
          <button class="kole-m-datepicker__btn kole-m-datepicker__btn--confirm" type="button">确定</button>
        </div>
        <div class="kole-m-datepicker__columns">
          <ul class="kole-m-datepicker__column" role="listbox" aria-label="年">
            <li class="kole-m-datepicker__option is-selected" role="option" aria-selected="true">2026</li>
          </ul>
          <ul class="kole-m-datepicker__column" role="listbox" aria-label="月">
            <li class="kole-m-datepicker__option is-selected" role="option" aria-selected="true">9 月</li>
          </ul>
          <ul class="kole-m-datepicker__column" role="listbox" aria-label="日">
            <li class="kole-m-datepicker__option is-selected" role="option" aria-selected="true">20</li>
          </ul>
        </div>
      </div>
    </div>
  </section>
</div>
<script>
  /* 演示页交互:取消按钮只打标记(真实业务里由宿主关闭浮层并丢弃选择)。 */
  (function () {
    var cancel = document.getElementById('dp-cancel');
    var dialog = document.getElementById('dp-date');
    if (!cancel || !dialog) return;
    cancel.addEventListener('click', function () {
      dialog.setAttribute('data-cancelled', 'true');
    });
  })();
</script>
<script>
  /* ?demo=<id> → 只显示该演示块(文档站按块预览用;无参数时全部显示,测试与回归走无参数路径) */
  (function () {
    var id = new URLSearchParams(location.search).get('demo');
    if (!id) return;
    var blocks = Array.prototype.slice.call(document.querySelectorAll('.demo-block'));
    var hit = false;
    blocks.forEach(function (b) {
      var on = b.getAttribute('data-demo') === id;
      if (on) hit = true;
      b.hidden = !on;
    });
    if (!hit) { blocks.forEach(function (b) { b.hidden = false; }); return; }
    document.body.classList.add('demo-single');
    blocks.forEach(function (b) {
      var label = b.querySelector('.demo-label');
      if (label && !b.hidden) label.hidden = true;
    });
  })();
</script>
</body>
</html>
frameworks-mobile/DatePicker.jsx · React · 140 行
frameworks-mobile/DatePicker.jsx
import React, { useRef, useState } from 'react';
import './DatePicker.css';

/* 日期选择器(移动端)— 规格 §18。
   底部浮层 = 装饰性遮罩(aria-hidden)+ 面板;遮罩点击关闭,closeOnMask=false 时不关(规格 §18.5)。
   面板 role="dialog" + aria-modal="true",标题元素 id 由 aria-labelledby 指向(规格 §18.6)。
   每列 role="listbox"、选项 role="option" + aria-selected;选中项的 is-selected 与 aria-selected 同步
   (选中值以 YYYY-MM-DD 文本呈现:value 直接消费,不依赖视觉滚动位置)。
   本端不发明选择事件(契约只有 confirm / close):点选只改本地高亮,确认时由宿主读 confirm 后的语义。 */

/* 面板标题元素 id:同页多实例不撞号(不依赖 React 版本特性,模块级自增即可) */
let seq = 0;

const COLUMN_LABELS = { year: '年', month: '月', day: '日' };

/* value 形如 YYYY-MM-DD;mode=month 时只有 YYYY-MM,day 为空串 */
function partsOf(value) {
  const seg = String(value || '').split('-');
  return { year: seg[0] || '', month: seg[1] || '', day: seg[2] || '' };
}

/* 选项文字可能带单位(「9 月」「9月」),取首个数字段比较;纯符号选项退回逐字比较 */
function sameValue(option, part) {
  if (part === undefined || part === null || part === '') return false;
  const digits = String(option).match(/[0-9]+/);
  if (digits) return String(Number(digits[0])) === String(Number(part));
  return String(option) === String(part);
}

/* mode=date → 年/月/日 三列;mode=month → 年/月 两列(规格 §18.3) */
function columnsOf(mode, years, months, days) {
  const cols = [
    { key: 'year', label: COLUMN_LABELS.year, options: years },
    { key: 'month', label: COLUMN_LABELS.month, options: months }
  ];
  if (mode !== 'month') cols.push({ key: 'day', label: COLUMN_LABELS.day, options: days });
  return cols;
}

export default function DatePicker({
  mode = 'date',
  round = false,
  open = false,
  closeOnMask = true,
  value = '',
  years = [],
  months = [],
  days = [],
  onConfirm,
  onClose,
  children = null,
}) {
  const idRef = useRef(null);
  if (idRef.current === null) {
    seq += 1;
    idRef.current = 'kole-m-datepicker-title-' + seq;
  }
  const titleId = idRef.current;

  /* null = 跟随 value 属性;点选后本地记账,确认前不改宿主的值 */
  const [draft, setDraft] = useState(null);
  const picked = draft || partsOf(value);

  const title = mode === 'month' ? '选择月份' : '选择日期';
  const label = title + (value ? ':' + value : '');
  const columns = columnsOf(mode, years, months, days);

  function select(key, option) {
    setDraft(Object.assign({}, picked, { [key]: String(option) }));
  }

  return (
    <>
      {children}
      <div
        className={'kole-m-datepicker__mask' + (open ? ' is-open' : '')}
        aria-hidden="true"
        onClick={() => {
          if (closeOnMask && onClose) onClose();
        }}
      />
      <div
        className={
          'kole-m-datepicker' +
          (round ? ' kole-m-datepicker--round' : '') +
          (open ? ' is-open' : '')
        }
        role="dialog"
        aria-modal="true"
        aria-labelledby={titleId}
        aria-hidden={open ? undefined : 'true'}
      >
        <div className="kole-m-datepicker__header">
          <button
            className="kole-m-datepicker__btn"
            type="button"
            onClick={() => {
              if (onClose) onClose();
            }}
          >
            取消
          </button>
          <span className="kole-m-datepicker__title" id={titleId}>
            {label}
          </span>
          <button
            className="kole-m-datepicker__btn kole-m-datepicker__btn--confirm"
            type="button"
            onClick={() => {
              if (onConfirm) onConfirm();
            }}
          >
            确定
          </button>
        </div>
        <div className="kole-m-datepicker__columns">
          {columns.map((col) => (
            <ul className="kole-m-datepicker__column" role="listbox" aria-label={col.label} key={col.key}>
              {col.options.map((option, i) => {
                const selected = sameValue(option, picked[col.key]);
                return (
                  <li
                    className={'kole-m-datepicker__option' + (selected ? ' is-selected' : '')}
                    role="option"
                    aria-selected={selected ? 'true' : 'false'}
                    key={String(option) + i}
                    onClick={() => select(col.key, option)}
                  >
                    {option}
                  </li>
                );
              })}
            </ul>
          ))}
        </div>
      </div>
    </>
  );
}
frameworks-mobile/DatePicker.vue2.vue · Vue 2 · 158 行
frameworks-mobile/DatePicker.vue2.vue
<template>
  <div>
    <slot></slot>
    <div
      class="kole-m-datepicker__mask"
      :class="{ 'is-open': open }"
      aria-hidden="true"
      @click="onMaskClick"
    ></div>
    <div
      class="kole-m-datepicker"
      :class="panelClass"
      role="dialog"
      aria-modal="true"
      :aria-labelledby="titleId"
      :aria-hidden="open ? null : 'true'"
    >
      <div class="kole-m-datepicker__header">
        <button class="kole-m-datepicker__btn" type="button" @click="onCancelClick">取消</button>
        <span class="kole-m-datepicker__title" :id="titleId">{{ label }}</span>
        <button
          class="kole-m-datepicker__btn kole-m-datepicker__btn--confirm"
          type="button"
          @click="onConfirmClick"
        >确定</button>
      </div>
      <div class="kole-m-datepicker__columns">
        <ul
          v-for="col in columns"
          :key="col.key"
          class="kole-m-datepicker__column"
          role="listbox"
          :aria-label="col.label"
        >
          <li
            v-for="(option, i) in col.options"
            :key="col.key + '-' + i"
            class="kole-m-datepicker__option"
            :class="{ 'is-selected': isSelected(col.key, option) }"
            role="option"
            :aria-selected="isSelected(col.key, option) ? 'true' : 'false'"
            @click="select(col.key, option)"
          >{{ option }}</li>
        </ul>
      </div>
    </div>
  </div>
</template>

<script>
/* 日期选择器(移动端)— 规格 §18。
   底部浮层 = 装饰性遮罩(aria-hidden)+ 面板;遮罩点击关闭,closeOnMask=false 时不关(规格 §18.5)。
   面板 role="dialog" + aria-modal="true",标题元素 id 由 aria-labelledby 指向(规格 §18.6)。
   每列 role="listbox"、选项 role="option" + aria-selected;is-selected 与 aria-selected 同步
   (选中值以 YYYY-MM-DD 文本呈现:value 直接消费,不依赖视觉滚动位置)。
   本端不发明选择事件(契约只有 confirm / close):点选只改本地高亮。 */

var COLUMN_LABELS = { year: '年', month: '月', day: '日' };

/* 面板标题元素 id 的自增种子:同页多实例不撞号(不依赖内部实例字段) */
var titleSeed = 0;

/* value 形如 YYYY-MM-DD;mode=month 时只有 YYYY-MM,day 为空串 */
function partsOf(value) {
  var seg = String(value || '').split('-');
  return { year: seg[0] || '', month: seg[1] || '', day: seg[2] || '' };
}

/* 选项文字可能带单位(「9 月」),取首个数字段比较;纯符号选项退回逐字比较 */
function sameValue(option, part) {
  if (part === undefined || part === null || part === '') return false;
  var digits = String(option).match(/[0-9]+/);
  if (digits) return String(Number(digits[0])) === String(Number(part));
  return String(option) === String(part);
}

/* mode=date → 年/月/日 三列;mode=month → 年/月 两列(规格 §18.3) */
function columnsOf(mode, years, months, days) {
  var cols = [
    { key: 'year', label: COLUMN_LABELS.year, options: years },
    { key: 'month', label: COLUMN_LABELS.month, options: months }
  ];
  if (mode !== 'month') cols.push({ key: 'day', label: COLUMN_LABELS.day, options: days });
  return cols;
}

export default {
  name: 'KoleMDatePicker',
  props: {
    mode: { type: String, default: 'date' },
    round: { type: Boolean, default: false },
    open: { type: Boolean, default: false },
    closeOnMask: { type: Boolean, default: true },
    value: { type: String, default: '' },
    years: { type: Array, default: function () { return []; } },
    months: { type: Array, default: function () { return []; } },
    days: { type: Array, default: function () { return []; } }
  },
  data: function () {
    /* 每个实例占一个号,标题 id 因此唯一(aria-labelledby 指向它) */
    titleSeed += 1;
    return {
      instanceId: titleSeed,
      /* null = 跟随 value 属性;点选后本地记账,确认前不改宿主的值 */
      draft: null
    };
  },
  computed: {
    titleId: function () {
      return 'kole-m-datepicker-title-' + this.instanceId;
    },
    title: function () {
      return this.mode === 'month' ? '选择月份' : '选择日期';
    },
    label: function () {
      return this.value ? this.title + ':' + this.value : this.title;
    },
    columns: function () {
      return columnsOf(this.mode, this.years, this.months, this.days);
    },
    picked: function () {
      return this.draft || partsOf(this.value);
    },
    panelClass: function () {
      return [
        this.round ? 'kole-m-datepicker--round' : '',
        this.open ? 'is-open' : ''
      ].filter(Boolean);
    }
  },
  methods: {
    isSelected: function (key, option) {
      return sameValue(option, this.picked[key]);
    },
    select: function (key, option) {
      var next = partsOf(this.value);
      next.year = this.picked.year;
      next.month = this.picked.month;
      next.day = this.picked.day;
      next[key] = String(option);
      this.draft = next;
    },
    onMaskClick: function () {
      if (!this.closeOnMask) return;
      this.$emit('close');
    },
    onCancelClick: function () {
      this.$emit('close');
    },
    onConfirmClick: function () {
      this.$emit('confirm');
    }
  }
};
</script>

<style src="./DatePicker.css"></style>
frameworks-mobile/DatePicker.vue3.vue · Vue 3 · 134 行
frameworks-mobile/DatePicker.vue3.vue
<template>
  <div>
    <slot></slot>
    <div
      class="kole-m-datepicker__mask"
      :class="{ 'is-open': open }"
      aria-hidden="true"
      @click="onMaskClick"
    ></div>
    <div
      class="kole-m-datepicker"
      :class="panelClass"
      role="dialog"
      aria-modal="true"
      :aria-labelledby="titleId"
      :aria-hidden="open ? null : 'true'"
    >
      <div class="kole-m-datepicker__header">
        <button class="kole-m-datepicker__btn" type="button" @click="onCancelClick">取消</button>
        <span class="kole-m-datepicker__title" :id="titleId">{{ label }}</span>
        <button
          class="kole-m-datepicker__btn kole-m-datepicker__btn--confirm"
          type="button"
          @click="onConfirmClick"
        >确定</button>
      </div>
      <div class="kole-m-datepicker__columns">
        <ul
          v-for="col in columns"
          :key="col.key"
          class="kole-m-datepicker__column"
          role="listbox"
          :aria-label="col.label"
        >
          <li
            v-for="(option, i) in col.options"
            :key="col.key + '-' + i"
            class="kole-m-datepicker__option"
            :class="{ 'is-selected': isSelected(col.key, option) }"
            role="option"
            :aria-selected="isSelected(col.key, option) ? 'true' : 'false'"
            @click="select(col.key, option)"
          >{{ option }}</li>
        </ul>
      </div>
    </div>
  </div>
</template>

<script setup>
/* 日期选择器(移动端)— 规格 §18。
   底部浮层 = 装饰性遮罩(aria-hidden)+ 面板;遮罩点击关闭,closeOnMask=false 时不关(规格 §18.5)。
   面板 role="dialog" + aria-modal="true",标题元素 id 由 aria-labelledby 指向(规格 §18.6)。
   每列 role="listbox"、选项 role="option" + aria-selected;is-selected 与 aria-selected 同步
   (选中值以 YYYY-MM-DD 文本呈现:value 直接消费,不依赖视觉滚动位置)。
   本端不发明选择事件(契约只有 confirm / close):点选只改本地高亮。 */
import { computed, getCurrentInstance, ref } from 'vue';

const props = defineProps({
  mode: { type: String, default: 'date' },
  round: { type: Boolean, default: false },
  open: { type: Boolean, default: false },
  closeOnMask: { type: Boolean, default: true },
  value: { type: String, default: '' },
  years: { type: Array, default: () => [] },
  months: { type: Array, default: () => [] },
  days: { type: Array, default: () => [] }
});
const emit = defineEmits(['confirm', 'close']);

const COLUMN_LABELS = { year: '年', month: '月', day: '日' };

/* 面板标题元素 id:取当前实例 uid,保证同页多实例不撞号(不依赖 Vue 3.5 才有的 useId) */
const titleId = 'kole-m-datepicker-title-' + getCurrentInstance().uid;

/* value 形如 YYYY-MM-DD;mode=month 时只有 YYYY-MM,day 为空串 */
function partsOf(value) {
  const seg = String(value || '').split('-');
  return { year: seg[0] || '', month: seg[1] || '', day: seg[2] || '' };
}

/* 选项文字可能带单位(「9 月」),取首个数字段比较;纯符号选项退回逐字比较 */
function sameValue(option, part) {
  if (part === undefined || part === null || part === '') return false;
  const digits = String(option).match(/[0-9]+/);
  if (digits) return String(Number(digits[0])) === String(Number(part));
  return String(option) === String(part);
}

/* null = 跟随 value 属性;点选后本地记账,确认前不改宿主的值 */
const draft = ref(null);
const picked = computed(() => draft.value || partsOf(props.value));
const title = computed(() => (props.mode === 'month' ? '选择月份' : '选择日期'));
const label = computed(() => (props.value ? title.value + ':' + props.value : title.value));

/* mode=date → 年/月/日 三列;mode=month → 年/月 两列(规格 §18.3) */
const columns = computed(() => {
  const cols = [
    { key: 'year', label: COLUMN_LABELS.year, options: props.years },
    { key: 'month', label: COLUMN_LABELS.month, options: props.months }
  ];
  if (props.mode !== 'month') cols.push({ key: 'day', label: COLUMN_LABELS.day, options: props.days });
  return cols;
});

const panelClass = computed(() => [
  props.round ? 'kole-m-datepicker--round' : '',
  props.open ? 'is-open' : ''
].filter(Boolean));

function isSelected(key, option) {
  return sameValue(option, picked.value[key]);
}

function select(key, option) {
  draft.value = Object.assign({}, picked.value, { [key]: String(option) });
}

function onMaskClick() {
  if (!props.closeOnMask) return;
  emit('close');
}

function onCancelClick() {
  emit('close');
}

function onConfirmClick() {
  emit('confirm');
}
</script>

<style src="./DatePicker.css"></style>
frameworks-mobile/DatePicker.uniapp.vue · uni-app(跨端:小程序 / App / H5) · 241 行
frameworks-mobile/DatePicker.uniapp.vue
<template>
  <view>
    <slot></slot>
    <view
      class="kole-m-datepicker__mask"
      :class="{ 'is-open': open }"
      aria-hidden="true"
      @tap="onMaskTap"
      @touchmove.stop.prevent
    ></view>
    <view
      class="kole-m-datepicker"
      :class="panelClass"
      :role="'dialog'"
      :aria-modal="'true'"
      :aria-labelledby="titleId"
      :aria-hidden="open ? 'false' : 'true'"
    >
      <view class="kole-m-datepicker__header">
        <view class="kole-m-datepicker__btn" role="button" @tap="onCancelTap">
          <text>取消</text>
        </view>
        <text class="kole-m-datepicker__title" :id="titleId">{{ label }}</text>
        <view
          class="kole-m-datepicker__btn kole-m-datepicker__btn--confirm"
          role="button"
          @tap="onConfirmTap"
        >
          <text>确定</text>
        </view>
      </view>
      <view class="kole-m-datepicker__columns">
        <scroll-view
          v-for="col in columns"
          :key="col.key"
          class="kole-m-datepicker__column"
          scroll-y
          :role="'listbox'"
          :aria-label="col.label"
        >
          <view
            v-for="(option, i) in col.options"
            :key="col.key + '-' + i"
            class="kole-m-datepicker__option"
            :class="isSelected(col.key, option) ? 'is-selected' : ''"
            :role="'option'"
            :aria-selected="isSelected(col.key, option) ? 'true' : 'false'"
            @tap="select(col.key, option)"
          >
            <text>{{ option }}</text>
          </view>
        </scroll-view>
      </view>
    </view>
  </view>
</template>

<script setup>
/* uni-app 端 · 日期选择器(移动端)— 规格 §18
   跨端差异:用 view / text;每列用 scroll-view(scroll-y)代替 ul/li,滚动由小程序/App 原生接管,
   role / aria-* 仍按规格 §18.6 保留(用 :role / :aria-* 绑定,H5 产物里生效)。
   点击一律 @tap;遮罩用 @touchmove.stop.prevent 阻止背景滚动穿透。
   面板标题元素 id 用组件实例 uid 拼出,同页多实例不撞号;aria-labelledby 指向它。
   本端不发明选择事件(契约只有 confirm / close):点选只改本地高亮。
   尺寸用 rpx(88rpx = 375pt 下的 44px 触控最小边长)。 */
import { computed, ref } from 'vue';

/* 面板标题元素 id 的自增种子:同页多实例不撞号(uni 端也不依赖内部实例字段) */
let titleSeed = 0;

const props = defineProps({
  mode: { type: String, default: 'date' },
  round: { type: Boolean, default: false },
  open: { type: Boolean, default: false },
  closeOnMask: { type: Boolean, default: true },
  value: { type: String, default: '' },
  years: { type: Array, default: () => [] },
  months: { type: Array, default: () => [] },
  days: { type: Array, default: () => [] }
});
const emit = defineEmits(['confirm', 'close']);

const COLUMN_LABELS = { year: '年', month: '月', day: '日' };

/* 面板标题元素 id:组件初始化时占一个号,保证同页多实例不撞号 */
titleSeed += 1;
const titleId = 'kole-m-datepicker-title-' + titleSeed;

/* value 形如 YYYY-MM-DD;mode=month 时只有 YYYY-MM,day 为空串 */
function partsOf(value) {
  const seg = String(value || '').split('-');
  return { year: seg[0] || '', month: seg[1] || '', day: seg[2] || '' };
}

/* 选项文字可能带单位(「9 月」),取首个数字段比较;纯符号选项退回逐字比较 */
function sameValue(option, part) {
  if (part === undefined || part === null || part === '') return false;
  const digits = String(option).match(/[0-9]+/);
  if (digits) return String(Number(digits[0])) === String(Number(part));
  return String(option) === String(part);
}

/* null = 跟随 value 属性;点选后本地记账,确认前不改宿主的值 */
const draft = ref(null);
const picked = computed(() => draft.value || partsOf(props.value));
const title = computed(() => (props.mode === 'month' ? '选择月份' : '选择日期'));
const label = computed(() => (props.value ? title.value + ':' + props.value : title.value));

/* mode=date → 年/月/日 三列;mode=month → 年/月 两列(规格 §18.3) */
const columns = computed(() => {
  const cols = [
    { key: 'year', label: COLUMN_LABELS.year, options: props.years },
    { key: 'month', label: COLUMN_LABELS.month, options: props.months }
  ];
  if (props.mode !== 'month') cols.push({ key: 'day', label: COLUMN_LABELS.day, options: props.days });
  return cols;
});

const panelClass = computed(() => [
  props.round ? 'kole-m-datepicker--round' : '',
  props.open ? 'is-open' : ''
].filter(Boolean));

function isSelected(key, option) {
  return sameValue(option, picked.value[key]);
}

function select(key, option) {
  draft.value = Object.assign({}, picked.value, { [key]: String(option) });
}

function onMaskTap() {
  if (!props.closeOnMask) return;
  emit('close');
}

function onCancelTap() {
  emit('close');
}

function onConfirmTap() {
  emit('confirm');
}
</script>

<style>
.kole-m-datepicker {
  --kole-m-touch-target: 88rpx;
  --kole-m-action-height: 112rpx;
  --kole-m-font-size-body: 32rpx;
  --kole-m-font-size-label: 28rpx;
  --kole-m-gutter: 32rpx;
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 2001;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  padding-bottom: constant(safe-area-inset-bottom);
  padding-bottom: env(safe-area-inset-bottom);
  background-color: var(--kole-color-card-bg);
}

.kole-m-datepicker__mask {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 2000;
  background-color: var(--kole-color-mask);
  opacity: 0;
}

.kole-m-datepicker__mask.is-open { opacity: 1; }

/* 变体 round=true:靠内容一侧切圆角 */
.kole-m-datepicker--round {
  border-top-left-radius: 16rpx;
  border-top-right-radius: 16rpx;
}

.kole-m-datepicker__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 16rpx var(--kole-m-gutter);
  border-bottom: 1rpx solid var(--kole-color-border);
  font-size: var(--kole-m-font-size-body);
}

.kole-m-datepicker__title {
  color: var(--kole-color-text-title);
  font-size: var(--kole-m-font-size-body);
}

.kole-m-datepicker__btn {
  display: flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  min-height: var(--kole-m-touch-target);
  padding: 0 16rpx;
  border-radius: 8rpx;
  color: var(--kole-color-brand);
  font-size: var(--kole-m-font-size-label);
}

.kole-m-datepicker__btn--confirm { font-weight: 500; }

.kole-m-datepicker__columns {
  display: flex;
  align-items: stretch;
}

.kole-m-datepicker__column {
  flex: 1;
  min-width: 0;
  max-height: 400rpx;
}

.kole-m-datepicker__option {
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: var(--kole-m-touch-target);
  color: var(--kole-color-text-body);
  font-size: var(--kole-m-font-size-label);
  text-align: center;
}

/* 状态 selected:当前选中项(品牌色高亮) */
.kole-m-datepicker__option.is-selected {
  color: var(--kole-color-brand);
  font-weight: 500;
}
</style>

测试与回归

断言在真实的 375×640 设备帧里跑(引擎与 PC 侧共用 tests/_runtime.js,触控行为动词来自移动端 tests/mobile/_behaviors.js)。

断言 14 条 · 全部通过 报告 2026-09-20 16:50:33

复现命令
node site/dev-server.js &
REG_BASE=http://127.0.0.1:3311 node tools/run-mobile-regression.mjs   # 全量 5 个组件
npm run verify:mobile-docs                                           # 本页内容完整性 + API 与源码一致性

设计契约

components/mobile-datepicker.json(点击展开原始 JSON)
{
  "schemaVersion": 1,
  "sourceKind": "authored-spec",
  "provenance": "authored-in-repo",
  "specFile": "spec/移动端规格.md",
  "confidence": "high",
  "specSection": "18 · 日期选择器 DatePicker",
  "slug": "mobile-datepicker",
  "name": "日期选择器 DatePicker",
  "semanticTypeCandidates": [
    "datepicker",
    "calendar",
    "wheel"
  ],
  "variantDimensions": [
    {
      "name": "mode",
      "values": [
        "date",
        "month"
      ]
    },
    {
      "name": "round",
      "values": [
        "false",
        "true"
      ]
    }
  ],
  "representativeVariants": [
    {
      "mode": "date",
      "round": "true",
      "label": "年月日三列"
    },
    {
      "mode": "month",
      "round": "true",
      "label": "年月两列"
    },
    {
      "mode": "date",
      "round": "false",
      "label": "直角(不含圆角)"
    }
  ],
  "anatomy": {
    "mask": "遮罩,点击关闭",
    "picker": "底部浮层",
    "picker__header": "取消 / 标题 / 确定",
    "picker__columns": "年 / 月 / 日三列",
    "picker__column": "单列,可滚动",
    "picker__option": "单个选项"
  },
  "structurePatterns": {
    "mode": "date(年月日)/ month(年月)",
    "round": "false / true"
  },
  "usageHints": [
    "从底部弹出的年 / 月 / 日选择器,用于生日、有效期这类需要日期输入的场景",
    "遮罩点击关闭;closeOnMask=false 时不关闭",
    "选项行高不小于 44px,滚动容器 -webkit-overflow-scrolling: touch",
    "确定 / 取消按钮热区不小于 44px",
    "浮层 role=\"dialog\" + aria-modal=\"true\"",
    "每列 role=\"listbox\",选项 role=\"option\" + aria-selected",
    "遮罩 aria-hidden=\"true\"",
    "选中值以 YYYY-MM-DD 文本呈现(不依赖视觉滚动位置)"
  ],
  "doNotInvent": [
    "日期范围的禁用规则(由宿主传入)",
    "滚轮惯性 / 吸附动画的物理参数"
  ],
  "unknowns": [
    "可选年份的范围",
    "是否支持「至今」这类特殊选项"
  ],
  "interaction": [
    "遮罩点击关闭;closeOnMask=false 时不关闭",
    "选项行高不小于 44px,滚动容器 -webkit-overflow-scrolling: touch",
    "确定 / 取消按钮热区不小于 44px"
  ],
  "accessibility": [
    "浮层 role=\"dialog\" + aria-modal=\"true\"",
    "每列 role=\"listbox\",选项 role=\"option\" + aria-selected",
    "遮罩 aria-hidden=\"true\"",
    "选中值以 YYYY-MM-DD 文本呈现(不依赖视觉滚动位置)"
  ],
  "api": {
    "source": "implementation",
    "note": "props / events / slots 为 6 端实现的公共接口(说明文字取自规格对应小节)。字段名与各端源码逐名核对:node tools/verify-mobile-docs.mjs",
    "requiredNote": "「必传」按严格定义:实现里**没有默认值**时才为 Y(本门禁逐条核对 props 与各端源码的默认值,防止契约与实现脱节)。",
    "props": [
      {
        "name": "mode",
        "type": "'date' | 'month'",
        "default": "'date'",
        "desc": "变体 mode:date 为年月日三列,month 为年月两列(规格 §18.3)",
        "required": false
      },
      {
        "name": "round",
        "type": "boolean",
        "default": "false",
        "desc": "变体 round:靠内容一侧是否切圆角(规格 §18.3)",
        "required": false
      },
      {
        "name": "open",
        "type": "boolean",
        "default": "false",
        "desc": "状态 open:浮层是否展开(规格 §18.4)",
        "required": false
      },
      {
        "name": "closeOnMask",
        "type": "boolean",
        "default": "true",
        "desc": "遮罩点击是否关闭(规格 §18.5)",
        "required": false
      },
      {
        "name": "value",
        "type": "string",
        "default": "''",
        "desc": "当前选中值,YYYY-MM-DD(mode=month 时 YYYY-MM);用于让选中项可见(规格 §18.6)",
        "required": false
      },
      {
        "name": "years",
        "type": "Array",
        "default": "[]",
        "desc": "年份列选项(宿主提供,本组件不发明年份范围)(规格 §18.7)",
        "required": false
      },
      {
        "name": "months",
        "type": "Array",
        "default": "[]",
        "desc": "月份列选项(规格 §18.2 picker__column)",
        "required": false
      },
      {
        "name": "days",
        "type": "Array",
        "default": "[]",
        "desc": "日期列选项;mode=month 时不渲染该列(规格 §18.3)",
        "required": false
      }
    ],
    "events": [
      {
        "name": "confirm",
        "params": "—",
        "desc": "点击确定时触发(规格 §18.2 picker__header)"
      },
      {
        "name": "close",
        "params": "—",
        "desc": "点击取消或遮罩(closeOnMask=true 时)时触发(规格 §18.5)"
      }
    ],
    "slots": [
      {
        "name": "default",
        "desc": "自定义内容(追加在列区之后)"
      }
    ]
  },
  "variantClasses": {
    "mode": {
      "date": [],
      "month": []
    },
    "round": {
      "false": [],
      "true": [
        ".kole-m-datepicker--round"
      ]
    }
  },
  "demos": [
    {
      "id": "date",
      "group": "01 组件类型",
      "title": "年月日",
      "desc": "三列滚动,选中项用品牌色高亮并以 aria-selected 标注,不依赖视觉位置。",
      "variant": "mode=date"
    },
    {
      "id": "month",
      "group": "01 组件类型",
      "title": "年月",
      "desc": "只有两列,用于账单月份这类不需要具体日期的场景。",
      "variant": "mode=month"
    },
    {
      "id": "closed",
      "group": "02 组件状态",
      "title": "收起态",
      "desc": "默认态:遮罩 pointer-events: none,页面可正常滚动与点击。",
      "variant": "状态 closed"
    }
  ],
  "related": [
    {
      "slug": "actionsheet",
      "why": "从底部弹出的选择器与动作面板形态接近;选项是日期时才用日期选择器"
    },
    {
      "slug": "cell",
      "why": "「已选日期」的展示入口用单元格,点开才拉起选择器"
    },
    {
      "slug": "mobile-popup",
      "why": "日期选择器本身就是底部浮层,不要在弹出层里再套一层"
    }
  ]
}