移动端导航动作面板

动作面板ActionSheet

从底部弹出的操作列表,用于在少量互斥操作中做一次选择

反馈 规格 3 · 动作面板 ActionSheet 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/actionsheet.css">

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

演示

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

01 组件类型

标准操作组tone=default / cancel=separate

标题说明 + 操作项 + 独立成块的取消;操作项高度 56px。

查看代码(演示页原文 · 16 行)
frameworks-mobile/ActionSheet.html · default
<section class="demo-block" data-demo="default">
<p class="demo-label">标准操作组(tone=default / cancel=separate,点按钮打开)</p>
<div class="demo-frame" data-assert="actionsheet-default">
  <div class="demo-frame__body">
    <button class="kole-m-actionsheet__trigger" type="button"
            data-behavior="click-toggles-class:#as-standard|is-open">打开动作面板</button>
  </div>
  <div class="kole-m-actionsheet__mask" aria-hidden="true"></div>
  <div class="kole-m-actionsheet" id="as-standard" data-state="closed" role="dialog" aria-modal="true" aria-label="订单操作">
    <p class="kole-m-actionsheet__title">订单操作</p>
    <button class="kole-m-actionsheet__action" type="button">标记为已完成</button>
    <button class="kole-m-actionsheet__action" type="button">复制订单号</button>
    <button class="kole-m-actionsheet__action kole-m-actionsheet__cancel" type="button">取消</button>
  </div>
</div>
</section>
含危险操作tone=danger

危险项用错误色并补 aria-label;不要把它和普通项做成同色。

查看代码(演示页原文 · 15 行)
frameworks-mobile/ActionSheet.html · danger
<section class="demo-block" data-demo="danger">
<p class="demo-label">含危险操作(tone=danger,data-state 由 closed 变 open)</p>
<div class="demo-frame" data-assert="actionsheet-danger">
  <div class="demo-frame__body">
    <button class="kole-m-actionsheet__trigger" type="button"
            data-behavior="click-sets-attr:#as-danger|data-state|open">打开危险操作</button>
  </div>
  <div class="kole-m-actionsheet__mask" aria-hidden="true"></div>
  <div class="kole-m-actionsheet" id="as-danger" data-state="closed" role="dialog" aria-modal="true" aria-label="删除操作">
    <p class="kole-m-actionsheet__title">删除后不可恢复</p>
    <button class="kole-m-actionsheet__action kole-m-actionsheet__action--danger" type="button" aria-label="删除订单,不可恢复">删除订单</button>
    <button class="kole-m-actionsheet__action kole-m-actionsheet__cancel" type="button">取消</button>
  </div>
</div>
</section>
取消为普通项cancel=inline

操作项较多时取消不必独立成块,跟随列表一起滑出即可。

查看代码(演示页原文 · 12 行)
frameworks-mobile/ActionSheet.html · inline-cancel
<section class="demo-block" data-demo="inline-cancel">
<p class="demo-label">取消为普通项(cancel=inline,默认展开)</p>
<div class="demo-frame" data-assert="actionsheet-inline-cancel">
  <div class="kole-m-actionsheet__mask is-open" aria-hidden="true"></div>
  <div class="kole-m-actionsheet kole-m-actionsheet--inline is-open" role="dialog" aria-modal="true" aria-label="分享到">
    <p class="kole-m-actionsheet__title">分享到</p>
    <button class="kole-m-actionsheet__action" type="button">微信</button>
    <button class="kole-m-actionsheet__action" type="button">复制链接</button>
    <button class="kole-m-actionsheet__action kole-m-actionsheet__cancel" type="button">取消</button>
  </div>
</div>
</section>

02 组件状态

含禁用项disabled

禁用项置灰、点击无效;面板仍可关闭,不阻塞其它操作。

查看代码(演示页原文 · 12 行)
frameworks-mobile/ActionSheet.html · disabled
<section class="demo-block" data-demo="disabled">
<p class="demo-label">含禁用项(disabled 置灰且点击无效,默认展开)</p>
<div class="demo-frame" data-assert="actionsheet-disabled">
  <div class="kole-m-actionsheet__mask is-open" aria-hidden="true"></div>
  <div class="kole-m-actionsheet is-open" role="dialog" aria-modal="true" aria-label="批量操作">
    <button class="kole-m-actionsheet__action" type="button">批量导出</button>
    <button class="kole-m-actionsheet__action" type="button" disabled aria-label="批量归档(无权限)">批量归档</button>
    <button class="kole-m-actionsheet__action kole-m-actionsheet__cancel" type="button">取消</button>
  </div>
</div>
</div>
</section>

API

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

Props

名称类型默认值说明必传
visiblebooleanfalse面板开合(closed / open 由它表达,规格 §3.4)N
titlestring''可选标题说明(规格 §3.2)N
actionsAction[][]操作项:{ key, label, tone?, disabled?, ariaLabel? }(规格 §3.2/§3.3)N
cancelTextstring'取消'底部取消按钮文案(规格 §3.2)N
cancelStyle'separate' | 'inline''separate'变体 cancel:取消独立成块或作为普通项(规格 §3.3)N
labelstring'动作面板'面板的无障碍名称(规格 §3.6)N

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

事件

名称参数说明
selectkey, action点击某个操作项时触发(禁用项不触发)
close—点击遮罩、操作项或取消后触发

插槽

名称说明
—无插槽;结构由 title / actions / cancelText 驱动(规格 §3.2)

CSS 变量

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

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

何时使用

  • 从底部弹出的操作列表,用于在少量互斥操作中做一次选择
  • 点击遮罩关闭;点击操作项或取消后关闭
  • 面板滑出动画 240ms,缓动曲线 cubic-bezier(.32,.72,0,1)
  • 操作项点击热区高度不小于 56px
  • 面板 role=dialog + aria-modal=true,遮罩为纯装饰,不参与焦点
  • 操作项为原生 button,危险项带 aria-label 说明

交互与触控

  • 点击遮罩关闭;点击操作项或取消后关闭
  • 面板滑出动画 240ms,缓动曲线 cubic-bezier(.32,.72,0,1)
  • 操作项点击热区高度不小于 56px

无障碍

  • 面板 role=dialog + aria-modal=true
  • 遮罩为纯装饰,不参与焦点
  • 操作项为原生 button,危险项带 aria-label 说明

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

组件何时用它而不是本组件
滑动单元格SwipeCell单行操作用滑动单元格;整页级别的少量互斥操作用动作面板
底部标签栏TabBar面板从底部滑出,与底部标签栏同区域 —— 面板打开时应盖住它

规格未定 / 禁止发明

类别条目
禁止发明多级面板的堆叠规则
禁止发明手势下滑关闭的触发阈值
规格未定操作项超过多少条时需要内部滚动
规格未定危险项是否需要二次确认

结构(anatomy)

字段说明
mask遮罩,点击关闭
panel面板容器,自底部滑出
title可选标题说明
action操作项,高度 56px,可标记危险操作
cancel底部取消按钮,与操作项之间有间隔

变体维度与类名映射

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

维度取值对应类名 / 变量
tonedefault / danger
default (由数据驱动,无专属类)
danger .kole-m-actionsheet__action--danger
cancelinline / separate
inline .kole-m-actionsheet--inline
separate .kole-m-actionsheet__cancel

代表变体

变体标签
tone=default · cancel=separate标准操作组
tone=danger · cancel=separate含危险操作
tone=default · cancel=inline取消为普通项
tone=default · cancel=separate含禁用项

用到的令牌

构建时从本组件样式表扫描得出。蓝色为移动端自有令牌,绿色为继承的 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-gutter --kole-m-safe-bottom --kole-m-touch-target --kole-color-border --kole-color-brand-bg --kole-color-card-bg --kole-color-error --kole-color-focus-ring --kole-color-mask --kole-color-page-bg --kole-color-text-body --kole-color-text-disabled --kole-color-text-secondary --kole-ease-standard --kole-font-family --kole-radius-base --kole-space-12 --kole-space-8

6 端源码

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

frameworks-mobile/ActionSheet.css · 纯样式(CSS) · 109 行
frameworks-mobile/ActionSheet.css
/* Kole UI Mobile · ActionSheet 样式 — 对齐移动端规格 §3
   动作面板:自底部滑出,遮罩点击关闭;操作项高度 ≥ 56px;滑出 240ms。 */

.kole-m-actionsheet__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-ease-standard);
}

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

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

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

.kole-m-actionsheet__title {
  margin: 0;
  padding: var(--kole-space-12) var(--kole-m-gutter);
  background: var(--kole-color-card-bg);
  color: var(--kole-color-text-secondary);
  font-size: var(--kole-m-font-size-label);
  line-height: 1.4;
  text-align: center;
}

.kole-m-actionsheet__action {
  display: block;
  box-sizing: border-box;
  width: 100%;
  min-height: var(--kole-m-action-height);
  padding: 0 var(--kole-m-gutter);
  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;
  text-align: center;
  cursor: pointer;
  touch-action: manipulation;
}

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

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

/* 变体 tone=danger:危险操作用错误色 */
.kole-m-actionsheet__action--danger { color: var(--kole-color-error); }

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

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

/* 变体 cancel=separate:取消独立成块,与操作项之间留出间隔 */
.kole-m-actionsheet__cancel {
  margin-top: var(--kole-space-8);
  font-weight: 500;
}

.kole-m-actionsheet--inline .kole-m-actionsheet__cancel { margin-top: 0; }

/* 演示页触发器(非组件交付样式,仅用于打开面板) */
.kole-m-actionsheet__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-actionsheet__trigger:focus-visible {
  outline: 2px solid var(--kole-color-focus-ring);
  outline-offset: 2px;
}
frameworks-mobile/ActionSheet.html · H5 原生(无框架) · 138 行
frameworks-mobile/ActionSheet.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 · ActionSheet(H5)</title>
<link rel="stylesheet" href="../.design_library/kole-ui-mobile/colors_and_type.css">
<link rel="stylesheet" href="ActionSheet.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: 220px;
    overflow: hidden; transform: translateZ(0); background: var(--kole-color-card-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; }
</style>
</head>
<body>
<div class="demo">
  <section class="demo-block" data-demo="default">
  <p class="demo-label">标准操作组(tone=default / cancel=separate,点按钮打开)</p>
  <div class="demo-frame" data-assert="actionsheet-default">
    <div class="demo-frame__body">
      <button class="kole-m-actionsheet__trigger" type="button"
              data-behavior="click-toggles-class:#as-standard|is-open">打开动作面板</button>
    </div>
    <div class="kole-m-actionsheet__mask" aria-hidden="true"></div>
    <div class="kole-m-actionsheet" id="as-standard" data-state="closed" role="dialog" aria-modal="true" aria-label="订单操作">
      <p class="kole-m-actionsheet__title">订单操作</p>
      <button class="kole-m-actionsheet__action" type="button">标记为已完成</button>
      <button class="kole-m-actionsheet__action" type="button">复制订单号</button>
      <button class="kole-m-actionsheet__action kole-m-actionsheet__cancel" type="button">取消</button>
    </div>
  </div>
  </section>
  <section class="demo-block" data-demo="danger">
  <p class="demo-label">含危险操作(tone=danger,data-state 由 closed 变 open)</p>
  <div class="demo-frame" data-assert="actionsheet-danger">
    <div class="demo-frame__body">
      <button class="kole-m-actionsheet__trigger" type="button"
              data-behavior="click-sets-attr:#as-danger|data-state|open">打开危险操作</button>
    </div>
    <div class="kole-m-actionsheet__mask" aria-hidden="true"></div>
    <div class="kole-m-actionsheet" id="as-danger" data-state="closed" role="dialog" aria-modal="true" aria-label="删除操作">
      <p class="kole-m-actionsheet__title">删除后不可恢复</p>
      <button class="kole-m-actionsheet__action kole-m-actionsheet__action--danger" type="button" aria-label="删除订单,不可恢复">删除订单</button>
      <button class="kole-m-actionsheet__action kole-m-actionsheet__cancel" type="button">取消</button>
    </div>
  </div>
  </section>
  <section class="demo-block" data-demo="inline-cancel">
  <p class="demo-label">取消为普通项(cancel=inline,默认展开)</p>
  <div class="demo-frame" data-assert="actionsheet-inline-cancel">
    <div class="kole-m-actionsheet__mask is-open" aria-hidden="true"></div>
    <div class="kole-m-actionsheet kole-m-actionsheet--inline is-open" role="dialog" aria-modal="true" aria-label="分享到">
      <p class="kole-m-actionsheet__title">分享到</p>
      <button class="kole-m-actionsheet__action" type="button">微信</button>
      <button class="kole-m-actionsheet__action" type="button">复制链接</button>
      <button class="kole-m-actionsheet__action kole-m-actionsheet__cancel" type="button">取消</button>
    </div>
  </div>
  </section>
  <section class="demo-block" data-demo="disabled">
  <p class="demo-label">含禁用项(disabled 置灰且点击无效,默认展开)</p>
  <div class="demo-frame" data-assert="actionsheet-disabled">
    <div class="kole-m-actionsheet__mask is-open" aria-hidden="true"></div>
    <div class="kole-m-actionsheet is-open" role="dialog" aria-modal="true" aria-label="批量操作">
      <button class="kole-m-actionsheet__action" type="button">批量导出</button>
      <button class="kole-m-actionsheet__action" type="button" disabled aria-label="批量归档(无权限)">批量归档</button>
      <button class="kole-m-actionsheet__action kole-m-actionsheet__cancel" type="button">取消</button>
    </div>
  </div>
</div>
  </section>

<script>
  /* 演示页交互:触发器切换面板开合;遮罩与操作项点击关闭。
     is-open 类驱动视觉,data-state 属性作为状态标记(同一函数写入,不会分叉)。 */
  (function () {
    function setOpen(panel, open) {
      panel.classList.toggle('is-open', open);
      panel.setAttribute('data-state', open ? 'open' : 'closed');
      var mask = panel.parentElement.querySelector('.kole-m-actionsheet__mask');
      if (mask) mask.classList.toggle('is-open', open);
    }
    document.querySelectorAll('.kole-m-actionsheet__trigger').forEach(function (trig) {
      trig.addEventListener('click', function () {
        var panel = trig.parentElement.parentElement.querySelector('.kole-m-actionsheet');
        if (panel) setOpen(panel, !panel.classList.contains('is-open'));
      });
    });
    document.querySelectorAll('.kole-m-actionsheet__mask').forEach(function (mask) {
      mask.addEventListener('click', function () {
        var panel = mask.parentElement.querySelector('.kole-m-actionsheet');
        if (panel) setOpen(panel, false);
      });
    });
    document.querySelectorAll('.kole-m-actionsheet__action').forEach(function (btn) {
      btn.addEventListener('click', function () {
        var panel = btn.closest('.kole-m-actionsheet');
        if (panel) setOpen(panel, false);
      });
    });
  })();
</script>
  <script>
    /* ?demo=<id> → 只显示该演示块:文档站为每个演示单独起一个 375×640 预览帧。
       无参数时全部显示 —— 测试页与回归走无参数路径,行为不变。 */
    (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');
      /* 单块模式去掉 min-height:100vh —— 否则内容高度随帧高变化(帧高→vh→内容高)形成反馈环,
         自适应量到的永远是视口高度而不是内容高度。 */
      var demoBox = document.querySelector('.demo');
      if (demoBox) demoBox.style.minHeight = 'auto';
      /* 单块模式:帧内不再重复显示演示标题行(文档页已给出标题与说明) */
      blocks.forEach(function (b) {
        var label = b.querySelector('.demo-label');
        if (label && !b.hidden) label.hidden = true;
      });
    })();
  </script>
</body>
</html>
frameworks-mobile/ActionSheet.jsx · React · 64 行
frameworks-mobile/ActionSheet.jsx
import React from 'react';
import './ActionSheet.css';

/* 动作面板(移动端)— 规格 §3;滑出 240ms,遮罩点击关闭,操作项热区 ≥ 56px */
export default function ActionSheet({
  visible = false,
  title = '',
  actions = [],
  cancelText = '取消',
  cancelStyle = 'separate',
  onClose,
  onSelect,
  label = '动作面板',
}) {
  return (
    <>
      <div
        className={'kole-m-actionsheet__mask' + (visible ? ' is-open' : '')}
        aria-hidden="true"
        onClick={onClose}
      />
      <div
        className={
          'kole-m-actionsheet' +
          (cancelStyle === 'inline' ? ' kole-m-actionsheet--inline' : '') +
          (visible ? ' is-open' : '')
        }
        data-state={visible ? 'open' : 'closed'}
        role="dialog"
        aria-modal="true"
        aria-label={label}
      >
        {title ? <p className="kole-m-actionsheet__title">{title}</p> : null}
        {actions.map((a, i) => (
          <button
            key={a.key || i}
            className={
              'kole-m-actionsheet__action' +
              (a.tone === 'danger' ? ' kole-m-actionsheet__action--danger' : '')
            }
            type="button"
            disabled={!!a.disabled}
            aria-label={a.ariaLabel}
            onClick={() => {
              if (a.disabled) return;
              if (onSelect) onSelect(a.key, a);
              if (onClose) onClose();
            }}
          >
            {a.label}
          </button>
        ))}
        <button
          className="kole-m-actionsheet__action kole-m-actionsheet__cancel"
          type="button"
          onClick={onClose}
        >
          {cancelText}
        </button>
      </div>
    </>
  );
}
frameworks-mobile/ActionSheet.vue2.vue · Vue 2 · 61 行
frameworks-mobile/ActionSheet.vue2.vue
<template>
  <div
    class="kole-m-actionsheet__mask"
    :class="{ 'is-open': visible }"
    aria-hidden="true"
    @click="$emit('close')"
  ></div>
  <div
    class="kole-m-actionsheet"
    :class="[{ 'is-open': visible }, { 'kole-m-actionsheet--inline': cancelStyle === 'inline' }]"
    :data-state="visible ? 'open' : 'closed'"
    role="dialog"
    aria-modal="true"
    :aria-label="label"
  >
    <p class="kole-m-actionsheet__title" v-if="title">{{ title }}</p>
    <button
      v-for="(a, i) in actions"
      :key="a.key || i"
      class="kole-m-actionsheet__action"
      :class="{ 'kole-m-actionsheet__action--danger': a.tone === 'danger' }"
      type="button"
      :disabled="!!a.disabled"
      :aria-label="a.ariaLabel"
      @click="onAction(a)"
    >
      {{ a.label }}
    </button>
    <button
      class="kole-m-actionsheet__action kole-m-actionsheet__cancel"
      type="button"
      @click="$emit('close')"
    >
      {{ cancelText }}
    </button>
  </div>
</template>

<script>
export default {
  name: 'KoleMActionSheet',
  props: {
    visible: { type: Boolean, default: false },
    title: { type: String, default: '' },
    actions: { type: Array, default: function () { return []; } },
    cancelText: { type: String, default: '取消' },
    cancelStyle: { type: String, default: 'separate' },
    label: { type: String, default: '动作面板' }
  },
  methods: {
    onAction: function (a) {
      if (a.disabled) return;
      this.$emit('select', a.key, a);
      this.$emit('close');
    }
  }
};
</script>

<style src="./ActionSheet.css"></style>
frameworks-mobile/ActionSheet.vue3.vue · Vue 3 · 57 行
frameworks-mobile/ActionSheet.vue3.vue
<template>
  <div
    class="kole-m-actionsheet__mask"
    :class="{ 'is-open': visible }"
    aria-hidden="true"
    @click="emit('close')"
  ></div>
  <div
    class="kole-m-actionsheet"
    :class="[{ 'is-open': visible }, { 'kole-m-actionsheet--inline': cancelStyle === 'inline' }]"
    :data-state="visible ? 'open' : 'closed'"
    role="dialog"
    aria-modal="true"
    :aria-label="label"
  >
    <p class="kole-m-actionsheet__title" v-if="title">{{ title }}</p>
    <button
      v-for="(a, i) in actions"
      :key="a.key || i"
      class="kole-m-actionsheet__action"
      :class="{ 'kole-m-actionsheet__action--danger': a.tone === 'danger' }"
      type="button"
      :disabled="!!a.disabled"
      :aria-label="a.ariaLabel"
      @click="onAction(a)"
    >
      {{ a.label }}
    </button>
    <button
      class="kole-m-actionsheet__action kole-m-actionsheet__cancel"
      type="button"
      @click="emit('close')"
    >
      {{ cancelText }}
    </button>
  </div>
</template>

<script setup>
const props = defineProps({
  visible: { type: Boolean, default: false },
  title: { type: String, default: '' },
  actions: { type: Array, default: () => [] },
  cancelText: { type: String, default: '取消' },
  cancelStyle: { type: String, default: 'separate' },
  label: { type: String, default: '动作面板' }
});
const emit = defineEmits(['close', 'select']);
function onAction(a) {
  if (a.disabled) return;
  emit('select', a.key, a);
  emit('close');
}
</script>

<style src="./ActionSheet.css"></style>
frameworks-mobile/ActionSheet.uniapp.vue · uni-app(跨端:小程序 / App / H5) · 113 行
frameworks-mobile/ActionSheet.uniapp.vue
<template>
  <view v-if="visible">
    <view class="kole-m-actionsheet__mask" @tap="emit('close')" @touchmove.stop.prevent></view>
    <view
      class="kole-m-actionsheet kole-m-actionsheet--open"
      :class="cancelStyle === 'inline' ? 'kole-m-actionsheet--inline' : ''"
      :data-state="'open'"
      role="dialog"
      aria-modal="true"
      :aria-label="label"
    >
      <text v-if="title" class="kole-m-actionsheet__title">{{ title }}</text>
      <view
        v-for="(a, i) in actions"
        :key="a.key || i"
        class="kole-m-actionsheet__action"
        :class="a.tone === 'danger' ? 'kole-m-actionsheet__action--danger' : ''"
        :aria-disabled="a.disabled ? 'true' : 'false'"
        :aria-label="a.ariaLabel || a.label"
        @tap="onAction(a)"
      >
        <text>{{ a.label }}</text>
      </view>
      <view class="kole-m-actionsheet__action kole-m-actionsheet__cancel" role="button" @tap="emit('close')">
        <text>{{ cancelText }}</text>
      </view>
    </view>
  </view>
</template>

<script setup>
/* uni-app 端 · 动作面板(移动端)— 规格 §3
   跨端差异:滑出动画由 uni.createAnimation / CSS transition 二选一,
   这里用 CSS transition(H5 与 App 端均支持),面板挂载在页面内而非独立 document;
   遮罩用 @touchmove.stop.prevent 阻止背景滚动穿透(小程序习惯做法)。 */
const props = defineProps({
  visible: { type: Boolean, default: false },
  title: { type: String, default: '' },
  actions: { type: Array, default: () => [] },
  cancelText: { type: String, default: '取消' },
  cancelStyle: { type: String, default: 'separate' },
  label: { type: String, default: '动作面板' }
});
const emit = defineEmits(['close', 'select']);
function onAction(a) {
  if (a.disabled) return;
  emit('select', a.key, a);
  emit('close');
}
</script>

<style>
.kole-m-actionsheet {
  --kole-m-action-height: 112rpx;
  --kole-m-touch-target: 88rpx;
  --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;
  display: flex;
  flex-direction: column;
  padding-bottom: constant(safe-area-inset-bottom);
  padding-bottom: env(safe-area-inset-bottom);
  background-color: var(--kole-color-page-bg);
}

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

.kole-m-actionsheet__title {
  display: block;
  padding: 24rpx var(--kole-m-gutter);
  background-color: var(--kole-color-card-bg);
  color: var(--kole-color-text-secondary);
  font-size: var(--kole-m-font-size-label);
  text-align: center;
}

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

.kole-m-actionsheet__action--danger { color: var(--kole-color-error); }

.kole-m-actionsheet__action[aria-disabled="true"] { color: var(--kole-color-text-disabled); }

/* 变体 cancel=separate:取消独立成块 */
.kole-m-actionsheet__cancel {
  margin-top: 16rpx;
  font-weight: 600;
}

.kole-m-actionsheet--inline .kole-m-actionsheet__cancel { margin-top: 0; }
</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/actionsheet.json(点击展开原始 JSON)
{
  "schemaVersion": 1,
  "sourceKind": "authored-spec",
  "provenance": "authored-in-repo",
  "specFile": "spec/移动端规格.md",
  "specSection": "3 · 动作面板 ActionSheet",
  "confidence": "high",
  "slug": "actionsheet",
  "name": "动作面板 ActionSheet",
  "semanticTypeCandidates": [
    "sheet",
    "action-sheet",
    "dialog"
  ],
  "variantDimensions": [
    {
      "name": "tone",
      "values": [
        "default",
        "danger"
      ]
    },
    {
      "name": "cancel",
      "values": [
        "inline",
        "separate"
      ]
    }
  ],
  "representativeVariants": [
    {
      "tone": "default",
      "cancel": "separate",
      "label": "标准操作组"
    },
    {
      "tone": "danger",
      "cancel": "separate",
      "label": "含危险操作"
    },
    {
      "tone": "default",
      "cancel": "inline",
      "label": "取消为普通项"
    },
    {
      "tone": "default",
      "cancel": "separate",
      "label": "含禁用项"
    }
  ],
  "anatomy": {
    "mask": "遮罩,点击关闭",
    "panel": "面板容器,自底部滑出",
    "title": "可选标题说明",
    "action": "操作项,高度 56px,可标记危险操作",
    "cancel": "底部取消按钮,与操作项之间有间隔"
  },
  "structurePatterns": {
    "tone": "default / danger(危险项用错误色)",
    "cancel": "inline(取消作为普通项)/ separate(取消独立成块)"
  },
  "usageHints": [
    "从底部弹出的操作列表,用于在少量互斥操作中做一次选择",
    "点击遮罩关闭;点击操作项或取消后关闭",
    "面板滑出动画 240ms,缓动曲线 cubic-bezier(.32,.72,0,1)",
    "操作项点击热区高度不小于 56px",
    "面板 role=dialog + aria-modal=true,遮罩为纯装饰,不参与焦点",
    "操作项为原生 button,危险项带 aria-label 说明"
  ],
  "doNotInvent": [
    "多级面板的堆叠规则",
    "手势下滑关闭的触发阈值"
  ],
  "unknowns": [
    "操作项超过多少条时需要内部滚动",
    "危险项是否需要二次确认"
  ],
  "interaction": [
    "点击遮罩关闭;点击操作项或取消后关闭",
    "面板滑出动画 240ms,缓动曲线 cubic-bezier(.32,.72,0,1)",
    "操作项点击热区高度不小于 56px"
  ],
  "accessibility": [
    "面板 role=dialog + aria-modal=true",
    "遮罩为纯装饰,不参与焦点",
    "操作项为原生 button,危险项带 aria-label 说明"
  ],
  "api": {
    "source": "implementation",
    "note": "props / events / slots 为 6 端实现的公共接口(说明文字取自规格对应小节)。字段名与各端源码逐名核对:node tools/verify-mobile-docs.mjs",
    "props": [
      {
        "name": "visible",
        "type": "boolean",
        "default": "false",
        "desc": "面板开合(closed / open 由它表达,规格 §3.4)",
        "required": false
      },
      {
        "name": "title",
        "type": "string",
        "default": "''",
        "desc": "可选标题说明(规格 §3.2)",
        "required": false
      },
      {
        "name": "actions",
        "type": "Action[]",
        "default": "[]",
        "desc": "操作项:{ key, label, tone?, disabled?, ariaLabel? }(规格 §3.2/§3.3)",
        "required": false
      },
      {
        "name": "cancelText",
        "type": "string",
        "default": "'取消'",
        "desc": "底部取消按钮文案(规格 §3.2)",
        "required": false
      },
      {
        "name": "cancelStyle",
        "type": "'separate' | 'inline'",
        "default": "'separate'",
        "desc": "变体 cancel:取消独立成块或作为普通项(规格 §3.3)",
        "required": false
      },
      {
        "name": "label",
        "type": "string",
        "default": "'动作面板'",
        "desc": "面板的无障碍名称(规格 §3.6)",
        "required": false
      }
    ],
    "events": [
      {
        "name": "select",
        "params": "key, action",
        "desc": "点击某个操作项时触发(禁用项不触发)"
      },
      {
        "name": "close",
        "params": "—",
        "desc": "点击遮罩、操作项或取消后触发"
      }
    ],
    "slots": [
      {
        "name": "—",
        "desc": "无插槽;结构由 title / actions / cancelText 驱动(规格 §3.2)"
      }
    ],
    "requiredNote": "「必传」按严格定义:实现里**没有默认值**时才为 Y(本门禁逐条核对 props 与各端源码的默认值,防止契约与实现脱节)。"
  },
  "variantClasses": {
    "tone": {
      "default": [],
      "danger": [
        ".kole-m-actionsheet__action--danger"
      ]
    },
    "cancel": {
      "separate": [
        ".kole-m-actionsheet__cancel"
      ],
      "inline": [
        ".kole-m-actionsheet--inline"
      ]
    }
  },
  "demos": [
    {
      "id": "default",
      "group": "01 组件类型",
      "title": "标准操作组",
      "desc": "标题说明 + 操作项 + 独立成块的取消;操作项高度 56px。",
      "variant": "tone=default / cancel=separate"
    },
    {
      "id": "danger",
      "group": "01 组件类型",
      "title": "含危险操作",
      "desc": "危险项用错误色并补 aria-label;不要把它和普通项做成同色。",
      "variant": "tone=danger"
    },
    {
      "id": "inline-cancel",
      "group": "01 组件类型",
      "title": "取消为普通项",
      "desc": "操作项较多时取消不必独立成块,跟随列表一起滑出即可。",
      "variant": "cancel=inline"
    },
    {
      "id": "disabled",
      "group": "02 组件状态",
      "title": "含禁用项",
      "desc": "禁用项置灰、点击无效;面板仍可关闭,不阻塞其它操作。",
      "variant": "disabled"
    }
  ],
  "related": [
    {
      "slug": "swipecell",
      "why": "单行操作用滑动单元格;整页级别的少量互斥操作用动作面板"
    },
    {
      "slug": "tabbar",
      "why": "面板从底部滑出,与底部标签栏同区域 —— 面板打开时应盖住它"
    }
  ]
}