移动端导航对话框

对话框Dialog

需要用户确认或输入的中断式浮层:标题 + 内容 + 操作按钮组

反馈 规格 13 · 对话框 Dialog 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-dialog.css">

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

演示

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

01 组件类型

确认框variant=confirm

两个按钮等宽排列,取消在左、确认在右(与移动端平台惯例一致)。

查看代码(演示页原文 · 20 行)
frameworks-mobile/Dialog.html · confirm
<section class="demo-block" data-demo="confirm">
  <p class="demo-label">确认框(variant=confirm:取消 + 确认两个按钮等宽,点按钮打开)</p>
  <div class="demo-frame" data-assert="dialog-confirm">
    <div class="demo-frame__body">
      <button class="demo-trigger" type="button"
              data-behavior="click-toggles-class:#dialog-confirm-box|is-open">打开确认框</button>
    </div>
    <div class="kole-m-dialog__mask" aria-hidden="true"></div>
    <div class="kole-m-dialog" id="dialog-confirm-box" role="dialog" aria-modal="true" aria-labelledby="dialog-confirm-title">
      <div class="kole-m-dialog__header" id="dialog-confirm-title">确认提交订单?</div>
      <div class="kole-m-dialog__body">
        <p>提交后进入审核流程,期间不可修改收货信息。</p>
      </div>
      <div class="kole-m-dialog__footer">
        <button class="kole-m-dialog__btn" type="button">取消</button>
        <button class="kole-m-dialog__btn kole-m-dialog__btn--primary" type="button">确认提交</button>
      </div>
    </div>
  </div>
</section>
危险确认tone=danger

删除类操作把确认按钮转为错误色,让代价在视觉上先被看到。

查看代码(演示页原文 · 16 行)
frameworks-mobile/Dialog.html · danger
<section class="demo-block" data-demo="danger">
  <p class="demo-label">危险确认(tone=danger:确认按钮用错误色,默认展开对照)</p>
  <div class="demo-frame" data-assert="dialog-danger">
    <div class="kole-m-dialog__mask is-open" aria-hidden="true"></div>
    <div class="kole-m-dialog is-open" role="dialog" aria-modal="true" aria-labelledby="dialog-danger-title">
      <div class="kole-m-dialog__header" id="dialog-danger-title">删除这条记录?</div>
      <div class="kole-m-dialog__body">
        <p>删除后不可恢复,关联的附件也会一并移除。</p>
      </div>
      <div class="kole-m-dialog__footer">
        <button class="kole-m-dialog__btn" type="button">取消</button>
        <button class="kole-m-dialog__btn kole-m-dialog__btn--danger" type="button">删除</button>
      </div>
    </div>
  </div>
</section>
提示框variant=alert

variant=alert 只有一个按钮并铺满整行,用于「知道了」这类单向告知。

查看代码(演示页原文 · 15 行)
frameworks-mobile/Dialog.html · alert
<section class="demo-block" data-demo="alert">
  <p class="demo-label">提示框(variant=alert:只有一个按钮,铺满整行;默认展开)</p>
  <div class="demo-frame" data-assert="dialog-alert">
    <div class="kole-m-dialog__mask is-open" aria-hidden="true"></div>
    <div class="kole-m-dialog is-open" role="dialog" aria-modal="true" aria-labelledby="dialog-alert-title">
      <div class="kole-m-dialog__header" id="dialog-alert-title">提交成功</div>
      <div class="kole-m-dialog__body">
        <p>订单号 20260920-001,可在「我的订单」里查看进度。</p>
      </div>
      <div class="kole-m-dialog__footer">
        <button class="kole-m-dialog__btn kole-m-dialog__btn--primary" type="button">知道了</button>
      </div>
    </div>
  </div>
</section>

02 组件状态

提交中状态 loading

确认按钮进入禁用态并标记 aria-busy,避免重复提交。

查看代码(演示页原文 · 16 行)
frameworks-mobile/Dialog.html · loading
<section class="demo-block" data-demo="loading">
  <p class="demo-label">提交中(状态 loading:确认按钮禁用并显示加载文案;默认展开)</p>
  <div class="demo-frame" data-assert="dialog-loading">
    <div class="kole-m-dialog__mask is-open" aria-hidden="true"></div>
    <div class="kole-m-dialog is-open" role="dialog" aria-modal="true" aria-labelledby="dialog-loading-title">
      <div class="kole-m-dialog__header" id="dialog-loading-title">正在提交</div>
      <div class="kole-m-dialog__body">
        <p>请勿关闭页面,提交完成后会自动跳转。</p>
      </div>
      <div class="kole-m-dialog__footer">
        <button class="kole-m-dialog__btn" type="button" disabled>取消</button>
        <button class="kole-m-dialog__btn kole-m-dialog__btn--primary is-loading" type="button" disabled aria-busy="true">提交中…</button>
      </div>
    </div>
  </div>
</section>
遮罩不关闭closeOnMask=false

closeOnMask=false:已填内容不因一次误触就丢失。

查看代码(演示页原文 · 16 行)
frameworks-mobile/Dialog.html · keep-open
<section class="demo-block" data-demo="keep-open">
  <p class="demo-label">遮罩不关闭(closeOnMask=false:表单类内容不该因误触丢弃;默认展开)</p>
  <div class="demo-frame" data-assert="dialog-keep-open">
    <div class="kole-m-dialog__mask is-open" aria-hidden="true"></div>
    <div class="kole-m-dialog is-open" data-close-on-mask="false" role="dialog" aria-modal="true" aria-labelledby="dialog-keep-title">
      <div class="kole-m-dialog__header" id="dialog-keep-title">填写收货信息</div>
      <div class="kole-m-dialog__body">
        <p>已填写的内容会在误触遮罩时保留,只能用取消或确认离开。</p>
      </div>
      <div class="kole-m-dialog__footer">
        <button class="kole-m-dialog__btn" type="button">取消</button>
        <button class="kole-m-dialog__btn kole-m-dialog__btn--primary" type="button">保存</button>
      </div>
    </div>
  </div>
</section>

API

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

Props

名称类型默认值说明必传
variant'confirm' | 'alert''confirm'变体 variant:确认框(两按钮)/ 提示框(单按钮铺满)(规格 §13.3)N
tone'default' | 'danger''default'变体 tone:danger 时确认按钮用错误色(规格 §13.3)N
roundbooleanfalse变体 round:是否切圆角(规格 §13.3)N
openbooleanfalse状态 open:展开且遮罩可见(规格 §13.4)N
closeOnMaskbooleantrue遮罩点击是否关闭;表单类内容通常设为 false(规格 §13.5)N
titlestring''标题文字,同时作为 aria-labelledby 指向的内容(规格 §13.2 header)N
confirmTextstring'确定'确认按钮文案(规格 §13.2 footer)N
cancelTextstring'取消'取消按钮文案(规格 §13.2 footer)N
confirmDisabledbooleanfalse状态 loading:确认按钮进入加载态并禁用(规格 §13.4)N

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

事件

名称参数说明
confirm—点击确认按钮时触发(确认按钮禁用时不触发)
cancel—点击取消按钮或遮罩(closeOnMask=true 时)时触发

插槽

名称说明
default内容区(规格 §13.2 body)

CSS 变量

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

名称默认值说明
--kole-m-dialog-radiusvar(--kole-radius-large)组件内部默认值,可在业务侧覆盖
--kole-m-dialog-width80%组件内部默认值,可在业务侧覆盖

何时使用

  • 需要用户确认或输入的中断式浮层:标题 + 内容 + 操作按钮组
  • 遮罩点击关闭;closeOnMask=false 时不关闭
  • Esc 关闭(键盘可达时)
  • 按钮热区不小于 44px;操作按钮等宽排列
  • 对话框 role="dialog" + aria-modal="true" + aria-labelledby 指向标题
  • 遮罩 aria-hidden="true"

交互与触控

  • 遮罩点击关闭;closeOnMask=false 时不关闭
  • Esc 关闭(键盘可达时)
  • 按钮热区不小于 44px;操作按钮等宽排列

无障碍

  • 对话框 role="dialog" + aria-modal="true" + aria-labelledby 指向标题
  • 打开后焦点落在对话框内(本实现只标记 tabindex="-1" + role,焦点陷阱见 §13.7)
  • 遮罩 aria-hidden="true"

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

组件何时用它而不是本组件
动作面板ActionSheet从底部弹出的多选项列表用动作面板,对话框用于需要做决定的中断式确认
弹出层Popup需要自定义内容与方向时用弹出层,对话框只是它的一种固定形态
轻提示Toast只是告知结果、不需要用户决定时用轻提示,不要用对话框打断

规格未定 / 禁止发明

类别条目
禁止发明焦点陷阱(focus trap)的完整实现
禁止发明多对话框嵌套时的层级规则
规格未定对话框的最大宽度与最大高度
规格未定长内容是否在 body 内滚动

结构(anatomy)

字段说明
mask遮罩,点击可关(可配置)
dialog对话框本体,居中
header标题区
body内容区
footer操作按钮组(取消 / 确认)

变体维度与类名映射

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

维度取值对应类名 / 变量
variantconfirm / alert
confirm (由数据驱动,无专属类)
alert (由数据驱动,无专属类)
tonedefault / danger
default .kole-m-dialog__btn--primary
danger .kole-m-dialog__btn--danger
roundfalse / true
false (由数据驱动,无专属类)
true .kole-m-dialog--round

代表变体

变体标签
variant=confirm · tone=default · round=true确认框
variant=confirm · tone=danger · round=true危险确认
variant=alert · tone=default · round=true提示框(单按钮)

用到的令牌

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

--kole-m-action-height --kole-m-duration-slide --kole-m-ease-slide --kole-m-font-size-body --kole-m-font-size-label --kole-m-font-size-title --kole-m-gutter --kole-color-border --kole-color-brand --kole-color-card-bg --kole-color-error --kole-color-focus-ring --kole-color-mask --kole-color-table-header-bg --kole-color-text-body --kole-color-text-disabled --kole-color-text-secondary --kole-color-text-title --kole-font-family --kole-radius-large --kole-space-20 --kole-space-8 --kole-m-dialog-radius --kole-m-dialog-width

6 端源码

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

frameworks-mobile/Dialog.css · 纯样式(CSS) · 122 行
frameworks-mobile/Dialog.css
/* Kole UI Mobile · Dialog 样式 — 对齐移动端规格 §13
   对话框:卡片底 + 正文色(亮色 15.13:1 / 暗色 10.34:1,脚本实测);
   遮罩点击关闭(可配置);按钮等宽排列、热区 ≥44px。 */

.kole-m-dialog {
  --kole-m-dialog-radius: var(--kole-radius-large);
  --kole-m-dialog-width: 80%;
  position: fixed;
  left: 50%;
  top: 50%;
  z-index: 2001;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  width: var(--kole-m-dialog-width);
  max-width: var(--kole-m-dialog-width);
  max-height: 70%;
  transform: translate(-50%, -50%) scale(0.92);
  background: var(--kole-color-card-bg);
  color: var(--kole-color-text-body);
  font-family: var(--kole-font-family);
  font-size: var(--kole-m-font-size-body);
  opacity: 0;
  pointer-events: none;
  transition: transform var(--kole-m-duration-slide) var(--kole-m-ease-slide),
    opacity var(--kole-m-duration-slide) var(--kole-m-ease-slide);
}

.kole-m-dialog.is-open {
  transform: translate(-50%, -50%) scale(1);
  opacity: 1;
  pointer-events: auto;
}

/* 变体 round=true:切圆角(默认直角,与 Element 的移动端弹窗一致) */
.kole-m-dialog--round {
  border-radius: var(--kole-m-dialog-radius);
  overflow: hidden;
}

.kole-m-dialog__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-dialog__mask.is-open {
  opacity: 1;
  pointer-events: auto;
}

.kole-m-dialog__header {
  padding: var(--kole-space-20) var(--kole-m-gutter) var(--kole-space-8);
  color: var(--kole-color-text-title);
  font-size: var(--kole-m-font-size-title);
  font-weight: 500;
  line-height: 1.4;
  text-align: center;
}

.kole-m-dialog__body {
  flex: 1 1 auto;
  min-height: 0;
  padding: 0 var(--kole-m-gutter) var(--kole-space-20);
  overflow: auto;
  -webkit-overflow-scrolling: touch;
  color: var(--kole-color-text-secondary);
  font-size: var(--kole-m-font-size-label);
  line-height: 1.7;
  text-align: center;
}

.kole-m-dialog__body > p { margin: 0; }

/* 变体 variant=alert:只有一个按钮,铺满整行 */
.kole-m-dialog__footer {
  display: flex;
  gap: 1px;
  border-top: 1px solid var(--kole-color-border);
}

.kole-m-dialog__btn {
  flex: 1 1 0;
  box-sizing: border-box;
  min-height: var(--kole-m-action-height);
  padding: 0 var(--kole-space-8);
  border: 0;
  background: var(--kole-color-card-bg);
  color: var(--kole-color-text-body);
  font-family: inherit;
  font-size: var(--kole-m-font-size-body);
  line-height: 1.3;
  cursor: pointer;
  touch-action: manipulation;
}

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

.kole-m-dialog__btn:active { background: var(--kole-color-table-header-bg); }

/* 变体 tone=danger:确认按钮用错误色 */
.kole-m-dialog__btn--danger { color: var(--kole-color-error); }
.kole-m-dialog__btn--primary { color: var(--kole-color-brand); font-weight: 500; }

.kole-m-dialog__btn:disabled,
.kole-m-dialog__btn.is-disabled {
  color: var(--kole-color-text-disabled);
  cursor: not-allowed;
}

.kole-m-dialog__btn:disabled:active { background: var(--kole-color-card-bg); }

/* 状态 loading:确认按钮进入加载态(宿主传入 disabled + 文案变化) */
.kole-m-dialog__btn.is-loading { color: var(--kole-color-text-disabled); }
frameworks-mobile/Dialog.html · H5 原生(无框架) · 169 行
frameworks-mobile/Dialog.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 · Dialog(H5)</title>
<link rel="stylesheet" href="../.design_library/kole-ui-mobile/colors_and_type.css">
<link rel="stylesheet" href="Dialog.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); }
  .demo-frame { position: relative; max-width: 375px; margin: 0 auto; height: 240px;
    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-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; }
  .demo-trigger:focus-visible { outline: 2px solid var(--kole-color-focus-ring); outline-offset: 2px; }
  .demo-block[hidden] { display: none; }
</style>
</head>
<body>
<div class="demo">
  <section class="demo-block" data-demo="confirm">
    <p class="demo-label">确认框(variant=confirm:取消 + 确认两个按钮等宽,点按钮打开)</p>
    <div class="demo-frame" data-assert="dialog-confirm">
      <div class="demo-frame__body">
        <button class="demo-trigger" type="button"
                data-behavior="click-toggles-class:#dialog-confirm-box|is-open">打开确认框</button>
      </div>
      <div class="kole-m-dialog__mask" aria-hidden="true"></div>
      <div class="kole-m-dialog" id="dialog-confirm-box" role="dialog" aria-modal="true" aria-labelledby="dialog-confirm-title">
        <div class="kole-m-dialog__header" id="dialog-confirm-title">确认提交订单?</div>
        <div class="kole-m-dialog__body">
          <p>提交后进入审核流程,期间不可修改收货信息。</p>
        </div>
        <div class="kole-m-dialog__footer">
          <button class="kole-m-dialog__btn" type="button">取消</button>
          <button class="kole-m-dialog__btn kole-m-dialog__btn--primary" type="button">确认提交</button>
        </div>
      </div>
    </div>
  </section>

  <section class="demo-block" data-demo="danger">
    <p class="demo-label">危险确认(tone=danger:确认按钮用错误色,默认展开对照)</p>
    <div class="demo-frame" data-assert="dialog-danger">
      <div class="kole-m-dialog__mask is-open" aria-hidden="true"></div>
      <div class="kole-m-dialog is-open" role="dialog" aria-modal="true" aria-labelledby="dialog-danger-title">
        <div class="kole-m-dialog__header" id="dialog-danger-title">删除这条记录?</div>
        <div class="kole-m-dialog__body">
          <p>删除后不可恢复,关联的附件也会一并移除。</p>
        </div>
        <div class="kole-m-dialog__footer">
          <button class="kole-m-dialog__btn" type="button">取消</button>
          <button class="kole-m-dialog__btn kole-m-dialog__btn--danger" type="button">删除</button>
        </div>
      </div>
    </div>
  </section>

  <section class="demo-block" data-demo="alert">
    <p class="demo-label">提示框(variant=alert:只有一个按钮,铺满整行;默认展开)</p>
    <div class="demo-frame" data-assert="dialog-alert">
      <div class="kole-m-dialog__mask is-open" aria-hidden="true"></div>
      <div class="kole-m-dialog is-open" role="dialog" aria-modal="true" aria-labelledby="dialog-alert-title">
        <div class="kole-m-dialog__header" id="dialog-alert-title">提交成功</div>
        <div class="kole-m-dialog__body">
          <p>订单号 20260920-001,可在「我的订单」里查看进度。</p>
        </div>
        <div class="kole-m-dialog__footer">
          <button class="kole-m-dialog__btn kole-m-dialog__btn--primary" type="button">知道了</button>
        </div>
      </div>
    </div>
  </section>

  <section class="demo-block" data-demo="loading">
    <p class="demo-label">提交中(状态 loading:确认按钮禁用并显示加载文案;默认展开)</p>
    <div class="demo-frame" data-assert="dialog-loading">
      <div class="kole-m-dialog__mask is-open" aria-hidden="true"></div>
      <div class="kole-m-dialog is-open" role="dialog" aria-modal="true" aria-labelledby="dialog-loading-title">
        <div class="kole-m-dialog__header" id="dialog-loading-title">正在提交</div>
        <div class="kole-m-dialog__body">
          <p>请勿关闭页面,提交完成后会自动跳转。</p>
        </div>
        <div class="kole-m-dialog__footer">
          <button class="kole-m-dialog__btn" type="button" disabled>取消</button>
          <button class="kole-m-dialog__btn kole-m-dialog__btn--primary is-loading" type="button" disabled aria-busy="true">提交中…</button>
        </div>
      </div>
    </div>
  </section>

  <section class="demo-block" data-demo="keep-open">
    <p class="demo-label">遮罩不关闭(closeOnMask=false:表单类内容不该因误触丢弃;默认展开)</p>
    <div class="demo-frame" data-assert="dialog-keep-open">
      <div class="kole-m-dialog__mask is-open" aria-hidden="true"></div>
      <div class="kole-m-dialog is-open" data-close-on-mask="false" role="dialog" aria-modal="true" aria-labelledby="dialog-keep-title">
        <div class="kole-m-dialog__header" id="dialog-keep-title">填写收货信息</div>
        <div class="kole-m-dialog__body">
          <p>已填写的内容会在误触遮罩时保留,只能用取消或确认离开。</p>
        </div>
        <div class="kole-m-dialog__footer">
          <button class="kole-m-dialog__btn" type="button">取消</button>
          <button class="kole-m-dialog__btn kole-m-dialog__btn--primary" type="button">保存</button>
        </div>
      </div>
    </div>
  </section>
</div>
<script>
  /* 演示页交互:触发器切换对话框开合;遮罩点击关闭(closeOnMask=false 的示例不关)。
     is-open 类驱动视觉,同时同步遮罩。 */
  (function () {
    function setOpen(dialog, open) {
      dialog.classList.toggle('is-open', open);
      var mask = dialog.parentElement ? dialog.parentElement.querySelector('.kole-m-dialog__mask') : null;
      if (mask) mask.classList.toggle('is-open', open);
    }
    document.querySelectorAll('.demo-trigger').forEach(function (trig) {
      trig.addEventListener('click', function () {
        var box = trig.parentElement.parentElement.querySelector('.kole-m-dialog');
        if (box) setOpen(box, !box.classList.contains('is-open'));
      });
    });
    document.querySelectorAll('.kole-m-dialog__mask').forEach(function (mask) {
      mask.addEventListener('click', function () {
        var box = mask.parentElement.querySelector('.kole-m-dialog');
        if (box && box.getAttribute('data-close-on-mask') !== 'false') setOpen(box, false);
      });
    });
    document.querySelectorAll('.kole-m-dialog__btn').forEach(function (btn) {
      btn.addEventListener('click', function () {
        if (btn.disabled) return;
        var box = btn.closest('.kole-m-dialog');
        if (box) setOpen(box, false);
      });
    });
  })();
</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/Dialog.jsx · React · 78 行
frameworks-mobile/Dialog.jsx
import React from 'react';
import './Dialog.css';

/* 对话框(移动端)— 规格 §13;variant=confirm(两个按钮)/ alert(单个按钮铺满);
   遮罩点击关闭(closeOnMask=false 时不关);对话框 role=dialog + aria-modal + aria-labelledby。 */
export default function Dialog({
  variant = 'confirm',
  tone = 'default',
  round = false,
  open = false,
  closeOnMask = true,
  title = '',
  confirmText = '确定',
  cancelText = '取消',
  confirmDisabled = false,
  onConfirm,
  onCancel,
  children = null,
}) {
  const cls =
    'kole-m-dialog' +
    (round ? ' kole-m-dialog--round' : '') +
    (open ? ' is-open' : '');
  const titleId = 'kole-m-dialog-title';

  return (
    <>
      <div
        className={'kole-m-dialog__mask' + (open ? ' is-open' : '')}
        aria-hidden="true"
        onClick={() => {
          if (closeOnMask && onCancel) onCancel();
        }}
      />
      <div
        className={cls}
        role="dialog"
        aria-modal="true"
        aria-labelledby={titleId}
        data-close-on-mask={closeOnMask ? 'true' : 'false'}
      >
        <div className="kole-m-dialog__header" id={titleId}>
          {title}
        </div>
        <div className="kole-m-dialog__body">{children}</div>
        <div className="kole-m-dialog__footer">
          {variant === 'confirm' ? (
            <button
              className="kole-m-dialog__btn"
              type="button"
              onClick={() => {
                if (onCancel) onCancel();
              }}
            >
              {cancelText}
            </button>
          ) : null}
          <button
            className={
              'kole-m-dialog__btn ' +
              (tone === 'danger' ? 'kole-m-dialog__btn--danger' : 'kole-m-dialog__btn--primary')
            }
            type="button"
            disabled={confirmDisabled}
            aria-busy={confirmDisabled ? 'true' : undefined}
            onClick={() => {
              if (confirmDisabled) return;
              if (onConfirm) onConfirm();
            }}
          >
            {confirmText}
          </button>
        </div>
      </div>
    </>
  );
}
frameworks-mobile/Dialog.vue2.vue · Vue 2 · 74 行
frameworks-mobile/Dialog.vue2.vue
<template>
  <div>
    <div class="kole-m-dialog__mask" :class="{ 'is-open': open }" aria-hidden="true" @click="onMaskClick"></div>
    <div
      class="kole-m-dialog"
      :class="dialogClass"
      role="dialog"
      aria-modal="true"
      :aria-labelledby="titleId"
      :data-close-on-mask="closeOnMask ? 'true' : 'false'"
    >
      <div class="kole-m-dialog__header" :id="titleId">{{ title }}</div>
      <div class="kole-m-dialog__body"><slot></slot></div>
      <div class="kole-m-dialog__footer">
        <button
          v-if="variant === 'confirm'"
          class="kole-m-dialog__btn"
          type="button"
          @click="$emit('cancel')"
        >{{ cancelText }}</button>
        <button
          class="kole-m-dialog__btn"
          :class="tone === 'danger' ? 'kole-m-dialog__btn--danger' : 'kole-m-dialog__btn--primary'"
          type="button"
          :disabled="confirmDisabled"
          :aria-busy="confirmDisabled ? 'true' : null"
          @click="onConfirmClick"
        >{{ confirmText }}</button>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'KoleMDialog',
  props: {
    variant: { type: String, default: 'confirm' },
    tone: { type: String, default: 'default' },
    round: { type: Boolean, default: false },
    open: { type: Boolean, default: false },
    closeOnMask: { type: Boolean, default: true },
    title: { type: String, default: '' },
    confirmText: { type: String, default: '确定' },
    cancelText: { type: String, default: '取消' },
    confirmDisabled: { type: Boolean, default: false }
  },
  computed: {
    /* 标题 id 用固定值:同一时刻页面上只应有一个对话框(多对话框层级规则见规格 §13.7) */
    titleId: function () {
      return 'kole-m-dialog-title';
    },
    dialogClass: function () {
      return [
        this.round ? 'kole-m-dialog--round' : '',
        this.open ? 'is-open' : ''
      ].filter(Boolean);
    }
  },
  methods: {
    onMaskClick: function () {
      if (!this.closeOnMask) return;
      this.$emit('cancel');
    },
    onConfirmClick: function () {
      if (this.confirmDisabled) return;
      this.$emit('confirm');
    }
  }
};
</script>

<style src="./Dialog.css"></style>
frameworks-mobile/Dialog.vue3.vue · Vue 3 · 74 行
frameworks-mobile/Dialog.vue3.vue
<template>
  <div>
    <div class="kole-m-dialog__mask" :class="{ 'is-open': open }" aria-hidden="true" @click="onMaskClick"></div>
    <div
      class="kole-m-dialog"
      :class="dialogClass"
      role="dialog"
      aria-modal="true"
      :aria-labelledby="titleId"
      :data-close-on-mask="closeOnMask ? 'true' : 'false'"
    >
      <div class="kole-m-dialog__header" :id="titleId">{{ title }}</div>
      <div class="kole-m-dialog__body"><slot></slot></div>
      <div class="kole-m-dialog__footer">
        <button
          v-if="variant === 'confirm'"
          class="kole-m-dialog__btn"
          type="button"
          @click="onCancelClick"
        >{{ cancelText }}</button>
        <button
          class="kole-m-dialog__btn"
          :class="tone === 'danger' ? 'kole-m-dialog__btn--danger' : 'kole-m-dialog__btn--primary'"
          type="button"
          :disabled="confirmDisabled"
          :aria-busy="confirmDisabled ? 'true' : null"
          @click="onConfirmClick"
        >{{ confirmText }}</button>
      </div>
    </div>
  </div>
</template>

<script setup>
import { computed } from 'vue';

const props = defineProps({
  variant: { type: String, default: 'confirm' },
  tone: { type: String, default: 'default' },
  round: { type: Boolean, default: false },
  open: { type: Boolean, default: false },
  closeOnMask: { type: Boolean, default: true },
  title: { type: String, default: '' },
  confirmText: { type: String, default: '确定' },
  cancelText: { type: String, default: '取消' },
  confirmDisabled: { type: Boolean, default: false }
});
const emit = defineEmits(['confirm', 'cancel']);

/* 标题 id 用固定值:同一时刻页面上只应有一个对话框(多对话框层级规则见规格 §13.7,本实现不发明) */
const titleId = 'kole-m-dialog-title';

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

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

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

function onConfirmClick() {
  if (props.confirmDisabled) return;
  emit('confirm');
}
</script>

<style src="./Dialog.css"></style>
frameworks-mobile/Dialog.uniapp.vue · uni-app(跨端:小程序 / App / H5) · 152 行
frameworks-mobile/Dialog.uniapp.vue
<template>
  <view>
    <view class="kole-m-dialog__mask" :class="{ 'is-open': open }" aria-hidden="true" @tap="onMaskTap"></view>
    <view
      class="kole-m-dialog"
      :class="dialogClass"
      role="dialog"
      aria-modal="true"
      :aria-label="title"
      :data-close-on-mask="closeOnMask ? 'true' : 'false'"
    >
      <view class="kole-m-dialog__header"><text>{{ title }}</text></view>
      <view class="kole-m-dialog__body"><slot></slot></view>
      <view class="kole-m-dialog__footer">
        <view
          v-if="variant === 'confirm'"
          class="kole-m-dialog__btn"
          role="button"
          @tap="onCancelTap"
        ><text>{{ cancelText }}</text></view>
        <view
          class="kole-m-dialog__btn"
          :class="tone === 'danger' ? 'kole-m-dialog__btn--danger' : 'kole-m-dialog__btn--primary'"
          :role="confirmDisabled ? '' : 'button'"
          :aria-disabled="confirmDisabled ? 'true' : 'false'"
          @tap="onConfirmTap"
        ><text>{{ confirmText }}</text></view>
      </view>
    </view>
  </view>
</template>

<script setup>
/* uni-app 端 · 对话框(移动端)— 规格 §13
   跨端差异:用 view / text;按钮用 view + role="button"(小程序无 <button> 的行内语义,
   role 在 H5 产物里生效),点击用 @tap;尺寸用 rpx(2rpx ≈ 1px)。 */
import { computed } from 'vue';

const props = defineProps({
  variant: { type: String, default: 'confirm' },
  tone: { type: String, default: 'default' },
  round: { type: Boolean, default: false },
  open: { type: Boolean, default: false },
  closeOnMask: { type: Boolean, default: true },
  title: { type: String, default: '' },
  confirmText: { type: String, default: '确定' },
  cancelText: { type: String, default: '取消' },
  confirmDisabled: { type: Boolean, default: false }
});
const emit = defineEmits(['confirm', 'cancel']);

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

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

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

function onConfirmTap() {
  if (props.confirmDisabled) return;
  emit('confirm');
}
</script>

<style>
.kole-m-dialog {
  --kole-m-dialog-width: 80%;
  --kole-m-touch-target: 88rpx;
  --kole-m-action-height: 112rpx;
  --kole-m-font-size-body: 32rpx;
  --kole-m-font-size-title: 34rpx;
  --kole-m-font-size-label: 28rpx;
  --kole-m-gutter: 32rpx;
  position: fixed;
  left: 50%;
  top: 50%;
  z-index: 2001;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  width: var(--kole-m-dialog-width);
  max-height: 70%;
  transform: translate(-50%, -50%);
  background-color: var(--kole-color-card-bg);
  color: var(--kole-color-text-body);
  font-size: var(--kole-m-font-size-body);
}

.kole-m-dialog--round {
  border-radius: 16rpx;
  overflow: hidden;
}

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

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

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

.kole-m-dialog__header {
  padding: 40rpx var(--kole-m-gutter) 16rpx;
  color: var(--kole-color-text-title);
  font-size: var(--kole-m-font-size-title);
  text-align: center;
}

.kole-m-dialog__body {
  flex: 1;
  padding: 0 var(--kole-m-gutter) 40rpx;
  overflow: auto;
  color: var(--kole-color-text-secondary);
  font-size: var(--kole-m-font-size-label);
  text-align: center;
}

.kole-m-dialog__footer {
  display: flex;
  border-top: 2rpx solid var(--kole-color-border);
}

.kole-m-dialog__btn {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  min-height: var(--kole-m-action-height);
  background-color: var(--kole-color-card-bg);
  color: var(--kole-color-text-body);
  font-size: var(--kole-m-font-size-body);
}

.kole-m-dialog__btn--danger { color: var(--kole-color-error); }
.kole-m-dialog__btn--primary { color: var(--kole-color-brand); }
</style>

测试与回归

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

断言 16 条 · 全部通过 报告 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-dialog.json(点击展开原始 JSON)
{
  "schemaVersion": 1,
  "sourceKind": "authored-spec",
  "provenance": "authored-in-repo",
  "specFile": "spec/移动端规格.md",
  "confidence": "high",
  "specSection": "13 · 对话框 Dialog",
  "slug": "mobile-dialog",
  "name": "对话框 Dialog",
  "semanticTypeCandidates": [
    "dialog",
    "modal",
    "confirm"
  ],
  "variantDimensions": [
    {
      "name": "variant",
      "values": [
        "confirm",
        "alert"
      ]
    },
    {
      "name": "tone",
      "values": [
        "default",
        "danger"
      ]
    },
    {
      "name": "round",
      "values": [
        "false",
        "true"
      ]
    }
  ],
  "representativeVariants": [
    {
      "variant": "confirm",
      "tone": "default",
      "round": "true",
      "label": "确认框"
    },
    {
      "variant": "confirm",
      "tone": "danger",
      "round": "true",
      "label": "危险确认"
    },
    {
      "variant": "alert",
      "tone": "default",
      "round": "true",
      "label": "提示框(单按钮)"
    }
  ],
  "anatomy": {
    "mask": "遮罩,点击可关(可配置)",
    "dialog": "对话框本体,居中",
    "header": "标题区",
    "body": "内容区",
    "footer": "操作按钮组(取消 / 确认)"
  },
  "structurePatterns": {
    "variant": "confirm(两个按钮)/ alert(单按钮铺满)",
    "tone": "default / danger",
    "round": "false / true"
  },
  "usageHints": [
    "需要用户确认或输入的中断式浮层:标题 + 内容 + 操作按钮组",
    "遮罩点击关闭;closeOnMask=false 时不关闭",
    "Esc 关闭(键盘可达时)",
    "按钮热区不小于 44px;操作按钮等宽排列",
    "对话框 role=\"dialog\" + aria-modal=\"true\" + aria-labelledby 指向标题",
    "遮罩 aria-hidden=\"true\""
  ],
  "doNotInvent": [
    "焦点陷阱(focus trap)的完整实现",
    "多对话框嵌套时的层级规则"
  ],
  "unknowns": [
    "对话框的最大宽度与最大高度",
    "长内容是否在 body 内滚动"
  ],
  "interaction": [
    "遮罩点击关闭;closeOnMask=false 时不关闭",
    "Esc 关闭(键盘可达时)",
    "按钮热区不小于 44px;操作按钮等宽排列"
  ],
  "accessibility": [
    "对话框 role=\"dialog\" + aria-modal=\"true\" + aria-labelledby 指向标题",
    "打开后焦点落在对话框内(本实现只标记 tabindex=\"-1\" + role,焦点陷阱见 §13.7)",
    "遮罩 aria-hidden=\"true\""
  ],
  "api": {
    "source": "implementation",
    "note": "props / events / slots 为 6 端实现的公共接口(说明文字取自规格对应小节)。字段名与各端源码逐名核对:node tools/verify-mobile-docs.mjs",
    "requiredNote": "「必传」按严格定义:实现里**没有默认值**时才为 Y(本门禁逐条核对 props 与各端源码的默认值,防止契约与实现脱节)。",
    "props": [
      {
        "name": "variant",
        "type": "'confirm' | 'alert'",
        "default": "'confirm'",
        "desc": "变体 variant:确认框(两按钮)/ 提示框(单按钮铺满)(规格 §13.3)",
        "required": false
      },
      {
        "name": "tone",
        "type": "'default' | 'danger'",
        "default": "'default'",
        "desc": "变体 tone:danger 时确认按钮用错误色(规格 §13.3)",
        "required": false
      },
      {
        "name": "round",
        "type": "boolean",
        "default": "false",
        "desc": "变体 round:是否切圆角(规格 §13.3)",
        "required": false
      },
      {
        "name": "open",
        "type": "boolean",
        "default": "false",
        "desc": "状态 open:展开且遮罩可见(规格 §13.4)",
        "required": false
      },
      {
        "name": "closeOnMask",
        "type": "boolean",
        "default": "true",
        "desc": "遮罩点击是否关闭;表单类内容通常设为 false(规格 §13.5)",
        "required": false
      },
      {
        "name": "title",
        "type": "string",
        "default": "''",
        "desc": "标题文字,同时作为 aria-labelledby 指向的内容(规格 §13.2 header)",
        "required": false
      },
      {
        "name": "confirmText",
        "type": "string",
        "default": "'确定'",
        "desc": "确认按钮文案(规格 §13.2 footer)",
        "required": false
      },
      {
        "name": "cancelText",
        "type": "string",
        "default": "'取消'",
        "desc": "取消按钮文案(规格 §13.2 footer)",
        "required": false
      },
      {
        "name": "confirmDisabled",
        "type": "boolean",
        "default": "false",
        "desc": "状态 loading:确认按钮进入加载态并禁用(规格 §13.4)",
        "required": false
      }
    ],
    "events": [
      {
        "name": "confirm",
        "params": "—",
        "desc": "点击确认按钮时触发(确认按钮禁用时不触发)"
      },
      {
        "name": "cancel",
        "params": "—",
        "desc": "点击取消按钮或遮罩(closeOnMask=true 时)时触发"
      }
    ],
    "slots": [
      {
        "name": "default",
        "desc": "内容区(规格 §13.2 body)"
      }
    ]
  },
  "variantClasses": {
    "variant": {
      "confirm": [],
      "alert": []
    },
    "tone": {
      "default": [
        ".kole-m-dialog__btn--primary"
      ],
      "danger": [
        ".kole-m-dialog__btn--danger"
      ]
    },
    "round": {
      "false": [],
      "true": [
        ".kole-m-dialog--round"
      ]
    }
  },
  "demos": [
    {
      "id": "confirm",
      "group": "01 组件类型",
      "title": "确认框",
      "desc": "两个按钮等宽排列,取消在左、确认在右(与移动端平台惯例一致)。",
      "variant": "variant=confirm"
    },
    {
      "id": "danger",
      "group": "01 组件类型",
      "title": "危险确认",
      "desc": "删除类操作把确认按钮转为错误色,让代价在视觉上先被看到。",
      "variant": "tone=danger"
    },
    {
      "id": "alert",
      "group": "01 组件类型",
      "title": "提示框",
      "desc": "variant=alert 只有一个按钮并铺满整行,用于「知道了」这类单向告知。",
      "variant": "variant=alert"
    },
    {
      "id": "loading",
      "group": "02 组件状态",
      "title": "提交中",
      "desc": "确认按钮进入禁用态并标记 aria-busy,避免重复提交。",
      "variant": "状态 loading"
    },
    {
      "id": "keep-open",
      "group": "02 组件状态",
      "title": "遮罩不关闭",
      "desc": "closeOnMask=false:已填内容不因一次误触就丢失。",
      "variant": "closeOnMask=false"
    }
  ],
  "related": [
    {
      "slug": "actionsheet",
      "why": "从底部弹出的多选项列表用动作面板,对话框用于需要做决定的中断式确认"
    },
    {
      "slug": "mobile-popup",
      "why": "需要自定义内容与方向时用弹出层,对话框只是它的一种固定形态"
    },
    {
      "slug": "mobile-toast",
      "why": "只是告知结果、不需要用户决定时用轻提示,不要用对话框打断"
    }
  ]
}