动作面板ActionSheet
从底部弹出的操作列表,用于在少量互斥操作中做一次选择
反馈 规格 3 · 动作面板 ActionSheet 6 端实现 触摸优先
<!-- ① 令牌: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 组件类型
标题说明 + 操作项 + 独立成块的取消;操作项高度 56px。
查看代码(演示页原文 · 16 行)
<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>危险项用错误色并补 aria-label;不要把它和普通项做成同色。
查看代码(演示页原文 · 15 行)
<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>操作项较多时取消不必独立成块,跟随列表一起滑出即可。
查看代码(演示页原文 · 12 行)
<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 组件状态
禁用项置灰、点击无效;面板仍可关闭,不阻塞其它操作。
查看代码(演示页原文 · 12 行)
<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
| 名称 | 类型 | 默认值 | 说明 | 必传 |
|---|---|---|---|---|
visible | boolean | false | 面板开合(closed / open 由它表达,规格 §3.4) | N |
title | string | '' | 可选标题说明(规格 §3.2) | N |
actions | Action[] | [] | 操作项:{ key, label, tone?, disabled?, ariaLabel? }(规格 §3.2/§3.3) | N |
cancelText | string | '取消' | 底部取消按钮文案(规格 §3.2) | N |
cancelStyle | 'separate' | 'inline' | 'separate' | 变体 cancel:取消独立成块或作为普通项(规格 §3.3) | N |
label | string | '动作面板' | 面板的无障碍名称(规格 §3.6) | N |
「必传」按严格定义:实现里**没有默认值**时才为 Y(本门禁逐条核对 props 与各端源码的默认值,防止契约与实现脱节)。
事件
| 名称 | 参数 | 说明 |
|---|---|---|
select | key, 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 校验(类/变量必须真实存在)。
| 维度 | 取值 | 对应类名 / 变量 |
|---|---|---|
tone | default / danger | default (由数据驱动,无专属类) danger .kole-m-actionsheet__action--danger |
cancel | inline / 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 令牌(改一处两端生效)。
6 端源码
同一组件的六份实现(生产环境的类名与结构一致,差异只在技术栈写法与单位)。点开查看,右侧可复制。
frameworks-mobile/ActionSheet.css · 纯样式(CSS) · 109 行
/* 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 行
<!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 行
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 行
<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 行
<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 行
<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": "面板从底部滑出,与底部标签栏同区域 —— 面板打开时应盖住它"
}
]
}