Files
aurora-admin/site/m/component/mobile-slider.html
T
aurora-admin f1fbfc2ddb
Regression / regression (push) Canceled after 0s
feat(品牌标识): 几何 K 图标(favicon/顶栏标记/theme-color) + 并行会话成果入库
## 品牌标识(本次会话)

起因:品牌此前没有任何图形标识 —— 唯一 favicon 是内联 data-URI 里的字母「A」,
那是 v2.0.0「Aurora Admin → Kole UI」改名漏掉的一处(PC 顶栏也是「A」,
移动端站已是「K」;移动端文档站则完全没有 favicon)。

- 几何:24 网格三个互不接触的笔画(竖 + 两斜),圆头描边;
  描边 2.25 → 16px 标签页尺寸下正好 1.5px = 规范原文「描边1.5px」
- 取色分两套(刻意):favicon 硬编码品牌蓝/白(渲染在浏览器标签栏,不继承 kole-dark);
  顶栏标记走 currentColor(实测暗色下自动转 rgb(20,22,28))
- 新增 theme-color 双条(light #FFFFFF / dark #1C1F26,取 --kole-color-card-bg)
- 修 site/app.js hero 标语 KOLE ADMIN → KOLE UI(改名变形残留)
- 移动端 7 个模板补 favicon(此前计数 0)

验收:门禁 9 条全 OK(site-routing/site-routes/mobile-docs/mobile-site/isolation/
theme/nav/i18n/icons);PC 回归 1464/1464 · 移动端 807/807,各连跑 8 次一致;
两端 favicon 405 字节逐字节一致;PC 站控制台错误 1→0。

## 并行会话成果(本次一并入库)

- 图标系统:2576 图标(TDesign/Element Plus,MIT)+ 11 端注入 + 5 个构建门禁工具
  + IconPreview 预览页 + ICON-SPEC.md 冻结规格
- 移动端平台:47 组件 × 6 端 + 文档站 53 页 + 隔离门禁
- PC 组件:103 个大后台组件 / 组件11 批次
- uni-app:PC 端试点 + 移动端端实现 + 真实编译验证

## 工程

- .gitignore 补 .scratch/ 与 .zcode-preexisting-*.txt(会话中间产物,实测 9.1MB,不入库)
- CHANGELOG 补品牌标识条目
- ROADMAP 登 S8-P4(品牌标识任务包 + og:image/apple-touch-icon 未做部分)
2026-09-21 10:05:48 +08:00

2169 lines
106 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script>
(function () {
var m = 'auto';
try { var v = localStorage.getItem('kole-mode'); if (v === 'light' || v === 'dark' || v === 'auto') m = v; } catch (e) {}
var dark = m === 'dark' || (m === 'auto' && window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches);
document.documentElement.classList.toggle('kole-dark', !!dark);
document.documentElement.style.colorScheme = dark ? 'dark' : 'light';
})();
</script>
<link rel="icon" href="data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3E%3Crect%20width='24'%20height='24'%20rx='6'%20fill='%232F54EB'/%3E%3Cg%20fill='none'%20stroke='%23FFFFFF'%20stroke-width='2.25'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Cpath%20d='M7%205.5V18.5'/%3E%3Cpath%20d='M17%205.5l-6.25%204.06'/%3E%3Cpath%20d='M10.75%2014.44L17%2018.5'/%3E%3C/g%3E%3C/svg%3E">
<meta name="theme-color" content="#FFFFFF" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#1C1F26" media="(prefers-color-scheme: dark)">
<title>滑动选择器 Slider — Kole UI Mobile</title>
<meta name="description" content="滑动选择器 Slider:移动端组件(6 端实现:CSS / H5 原生 / React / Vue 2 / Vue 3 / uni-app)。与 PC 端物理隔离:类名 kole-m-、令牌 --kole-m-。规格来源:移动端规格 35 · 滑动选择器 Slider。">
<link rel="stylesheet" href="../../../.design_library/kole-ui-mobile/colors_and_type.css">
<link rel="stylesheet" href="../style.css">
</head>
<body>
<header class="m-top">
<div class="m-top-inner">
<a class="m-logo" href="../index.html">
<span class="m-logo-mark" aria-hidden="true"><svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.25" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 5.5V18.5"/><path d="M17 5.5l-6.25 4.06"/><path d="M10.75 14.44L17 18.5"/></svg></span>
<span class="m-logo-text">Kole UI<em>移动端</em></span>
</a>
<div class="m-ver-picker" id="m-ver-picker">
<button id="m-ver-trigger" class="m-ver-trigger" type="button" data-single="1" aria-haspopup="listbox" aria-expanded="false" aria-label="选择版本" title="文档版本(清单来自部署根的 versions.json)">
<span id="m-ver">v1.0.0</span>
<svg class="m-ver-caret" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg>
</button>
<div id="m-ver-menu" class="m-ver-menu hidden" role="listbox" aria-label="文档版本">
<div class="m-ver-menu-head" id="m-ver-head">文档版本</div>
</div>
</div>
<nav class="m-nav"> <a href="../index.html" class="active" aria-current="page">组件总览</a>
<a href="../guide.html">快速开始</a>
<a href="../design.html">设计规范</a>
<a href="../faq.html">常见问题</a>
<a href="../changelog.html">更新日志</a></nav>
<div class="m-mode">
<button id="m-mode-trigger" class="m-mode-trigger" type="button" aria-haspopup="listbox" aria-expanded="false" aria-label="主题模式" title="主题模式">
<svg class="m-mode-icon" id="m-icon-moon" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
<svg class="m-mode-icon" id="m-icon-sun" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="display:none"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/></svg>
<svg class="m-mode-icon" id="m-icon-auto" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="display:none"><rect x="3" y="4" width="18" height="13" rx="2"/><path d="M8 21h8M12 17v4"/></svg>
<span id="m-mode-cur">自动模式</span>
<svg class="m-mode-caret" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg>
</button>
<div id="m-mode-menu" class="m-menu hidden" role="listbox" aria-label="主题模式">
<div class="m-menu-head">主题模式</div>
<button class="m-menu-opt" type="button" role="option" data-mode="light" aria-selected="false"><span data-mode-label="light">日间模式</span></button>
<button class="m-menu-opt" type="button" role="option" data-mode="dark" aria-selected="false"><span data-mode-label="dark">夜间模式</span></button>
<button class="m-menu-opt" type="button" role="option" data-mode="auto" aria-selected="true"><span data-mode-label="auto">自动模式</span></button>
</div>
</div>
</div>
</header>
<div class="m-body">
<details class="m-side" open>
<summary><span>移动端导航</span><span class="cur">滑动选择器</span></summary>
<nav>
<div class="m-side-section"><span>平台</span></div>
<a class="m-side-link m-side-platform" href="../../index.html"><span class="sp-top"><span class="sp-name">PC 端组件</span></span><span class="sp-sub">103 个中后台组件 · 5 端</span></a>
<a class="m-side-link m-side-platform" href="../index.html"><span class="sp-top"><span class="sp-name">移动端组件</span><span class="sp-badge">当前</span></span><span class="sp-sub">47 个组件 · 6 端 · 含 uni-app</span></a>
<div class="m-side-section"><span>开发指南</span></div>
<a class="m-side-link" href="../index.html">组件总览</a>
<a class="m-side-link" href="../guide.html">快速开始</a>
<a class="m-side-link" href="../design.html">设计规范</a>
<a class="m-side-link" href="../faq.html">常见问题</a>
<a class="m-side-link" href="../changelog.html">更新日志</a>
<a class="m-side-link" href="../platform.html">平台与端<em>移动端</em></a>
<a class="m-side-link" href="../../../tests/mobile/index.html" target="_blank">测试与回归<em>移动端</em></a>
<div class="m-side-section"><span>组件</span><span class="cnt">47</span></div>
<div class="m-side-group"><span>通用</span><span class="cnt">5</span></div>
<a class="m-side-link" href="mobile-button.html">按钮<em>Button</em></a>
<a class="m-side-link" href="mobile-divider.html">分割线<em>Divider</em></a>
<a class="m-side-link" href="mobile-icon.html">图标<em>Icon</em></a>
<a class="m-side-link" href="mobile-layout.html">布局<em>Layout</em></a>
<a class="m-side-link" href="mobile-link.html">链接<em>Link</em></a>
<div class="m-side-group"><span>导航</span><span class="cnt">6</span></div>
<a class="m-side-link" href="navbar.html">顶部导航栏<em>NavBar</em></a>
<a class="m-side-link" href="tabbar.html">底部标签栏<em>TabBar</em></a>
<a class="m-side-link" href="mobile-grid.html">宫格<em>Grid</em></a>
<a class="m-side-link" href="mobile-steps.html">步骤条<em>Steps</em></a>
<a class="m-side-link" href="mobile-segmented.html">分段器<em>Segmented</em></a>
<a class="m-side-link" href="mobile-sticky.html">吸顶容器<em>Sticky</em></a>
<div class="m-side-group"><span>数据录入</span><span class="cnt">16</span></div>
<a class="m-side-link" href="mobile-numberkeyboard.html">数字键盘<em>NumberKeyboard</em></a>
<a class="m-side-link" href="mobile-datepicker.html">日期选择器<em>DatePicker</em></a>
<a class="m-side-link" href="mobile-input.html">输入框<em>Input</em></a>
<a class="m-side-link" href="mobile-search.html">搜索框<em>Search</em></a>
<a class="m-side-link" href="mobile-stepper.html">步进器<em>Stepper</em></a>
<a class="m-side-link" href="mobile-switch.html">开关<em>Switch</em></a>
<a class="m-side-link" href="mobile-textarea.html">多行文本框<em>Textarea</em></a>
<a class="m-side-link" href="mobile-checkbox.html">多选框<em>Checkbox</em></a>
<a class="m-side-link" href="mobile-form.html">表单<em>Form</em></a>
<a class="m-side-link" href="mobile-radio.html">单选框<em>Radio</em></a>
<a class="m-side-link" href="mobile-rate.html">评分<em>Rate</em></a>
<a class="m-side-link is-active" href="mobile-slider.html" aria-current="page">滑动选择器<em>Slider</em></a>
<a class="m-side-link" href="mobile-cascader.html">级联选择器<em>Cascader</em></a>
<a class="m-side-link" href="mobile-colorpicker.html">颜色选择器<em>ColorPicker</em></a>
<a class="m-side-link" href="mobile-picker.html">选择器<em>Picker</em></a>
<a class="m-side-link" href="mobile-upload.html">上传<em>Upload</em></a>
<div class="m-side-group"><span>数据展示</span><span class="cnt">8</span></div>
<a class="m-side-link" href="cell.html">单元格<em>Cell</em></a>
<a class="m-side-link" href="mobile-badge.html">徽标<em>Badge</em></a>
<a class="m-side-link" href="mobile-tag.html">标签<em>Tag</em></a>
<a class="m-side-link" href="mobile-avatar.html">头像<em>Avatar</em></a>
<a class="m-side-link" href="mobile-list.html">列表<em>List</em></a>
<a class="m-side-link" href="mobile-collapse.html">折叠面板<em>Collapse</em></a>
<a class="m-side-link" href="mobile-table.html">表格<em>Table</em></a>
<a class="m-side-link" href="mobile-typography.html">排版<em>Typography</em></a>
<div class="m-side-group"><span>反馈</span><span class="cnt">12</span></div>
<a class="m-side-link" href="actionsheet.html">动作面板<em>ActionSheet</em></a>
<a class="m-side-link" href="pullrefresh.html">下拉刷新<em>PullRefresh</em></a>
<a class="m-side-link" href="swipecell.html">滑动单元格<em>SwipeCell</em></a>
<a class="m-side-link" href="mobile-popup.html">弹出层<em>Popup</em></a>
<a class="m-side-link" href="mobile-toast.html">轻提示<em>Toast</em></a>
<a class="m-side-link" href="mobile-dialog.html">对话框<em>Dialog</em></a>
<a class="m-side-link" href="mobile-noticebar.html">通知栏<em>NoticeBar</em></a>
<a class="m-side-link" href="mobile-loading.html">加载<em>Loading</em></a>
<a class="m-side-link" href="mobile-progress.html">进度条<em>Progress</em></a>
<a class="m-side-link" href="mobile-message.html">消息通知<em>Message</em></a>
<a class="m-side-link" href="mobile-overlay.html">遮罩层<em>Overlay</em></a>
<a class="m-side-link" href="mobile-popover.html">弹出气泡<em>Popover</em></a>
</nav>
</details>
<main class="m-main">
<section class="m-hero">
<h1>滑动选择器<span class="en" style="font-family: var(--kole-font-family-num); font-size: 14px; color: var(--kole-color-text-secondary); margin-left: 8px;">Slider</span></h1>
<p>在一个连续区间里用一次滑动选出数值(预算上限、价格区间、音量、亮度)</p>
<p style="margin-top: 8px;" class="m-chips">
<span class="m-chip m">数据录入</span>
<span class="m-chip">规格 35 · 滑动选择器 Slider</span>
<span class="m-chip">6 端实现</span>
<span class="m-chip">触摸优先</span>
</p>
<div class="m-code" style="margin-top: var(--kole-space-12);">
<div class="m-code-head"><span>引入(H5 原生;其余 5 端见「快速开始」)</span><button class="m-copy" type="button" data-copy="code-import">复制</button></div>
<pre id="code-import"><code>&lt;!-- ① 令牌:PC 令牌 + 移动端 --kole-m-* 合成单文件,引一次 --&gt;
&lt;link rel=&quot;stylesheet&quot; href=&quot;kole-ui/mobile/tokens.css&quot;&gt;
&lt;!-- ② 本组件样式(全量则用 kole-ui/mobile/components/index.css) --&gt;
&lt;link rel=&quot;stylesheet&quot; href=&quot;kole-ui/mobile/components/mobile-slider.css&quot;&gt;
&lt;!-- ③ 结构照抄下方任一演示块(类名与 6 端实现一致) --&gt;</code></pre>
</div>
</section>
<section class="m-block">
<h2 id="demo">演示</h2>
<p class="m-sub">每个演示都是真实渲染:预览帧加载 <code>frameworks-mobile/Slider.html?demo=&lt;id&gt;</code>(只显示该演示块),代码是该演示块在演示页里的原文,可复制。全部演示同屏可看 <a href="../../../frameworks-mobile/Slider.html" target="_blank">演示页 ↗</a>。</p>
<h3 class="m-demo-group">01 组件类型</h3>
<div class="m-demo" id="demo-basic">
<div class="m-demo-head"><span class="m-demo-title">基础用法</span><span class="m-chip">mode=single</span></div>
<p class="m-demo-desc">在轨道上按住拖动即改值;轨道可视 4px、热区 44px 高。</p>
<div class="m-demo-stage"><iframe src="../../../frameworks-mobile/Slider.html?demo=basic" title="滑动选择器 Slider · 基础用法" loading="lazy"></iframe></div>
<details class="m-details"><summary>查看代码(演示页原文 · 17 行)</summary>
<div class="m-code"><div class="m-code-head"><span>frameworks-mobile/Slider.html · basic</span><button class="m-copy" type="button" data-copy="code-demo-1">复制</button></div>
<pre id="code-demo-1"><code>&lt;section class=&quot;demo-block&quot; data-demo=&quot;basic&quot;&gt;
&lt;p class=&quot;demo-label&quot;&gt;基础用法(在轨道上按住拖动即改值;轨道可视 4px、热区 44px 高)&lt;/p&gt;
&lt;div class=&quot;demo-pad&quot;&gt;
&lt;div class=&quot;kole-m-slider&quot; id=&quot;sl-basic&quot; data-min=&quot;0&quot; data-max=&quot;100&quot; data-step=&quot;1&quot; data-value=&quot;40&quot;
data-assert=&quot;slider-basic&quot; data-behavior=&quot;swipe-sets-class:#sl-basic|is-dragging&quot;&gt;
&lt;span class=&quot;kole-m-slider__rail&quot; role=&quot;slider&quot; tabindex=&quot;0&quot;
aria-label=&quot;预算上限&quot; aria-valuemin=&quot;0&quot; aria-valuemax=&quot;100&quot; aria-valuenow=&quot;40&quot;&gt;
&lt;span class=&quot;kole-m-slider__track&quot;&gt;
&lt;span class=&quot;kole-m-slider__filled&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;kole-m-slider__thumb&quot; data-value=&quot;40&quot;&gt;&lt;/span&gt;
&lt;/span&gt;
&lt;/span&gt;
&lt;span class=&quot;kole-m-slider__value&quot;&gt;40&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p class=&quot;demo-note&quot;&gt;在轨道上按住即跳到手指处,横向拖动连续改值;松手后滑块 150ms 回弹到常态尺寸。&lt;/p&gt;
&lt;/section&gt;</code></pre></div>
</details>
</div>
<div class="m-demo" id="demo-steps">
<div class="m-demo-head"><span class="m-demo-title">带刻度</span><span class="m-chip">marks=with-marks</span></div>
<p class="m-demo-desc">marks=with-marks:轨道上标出等分点;取值仍按 step 吸附。</p>
<div class="m-demo-stage"><iframe src="../../../frameworks-mobile/Slider.html?demo=steps" title="滑动选择器 Slider · 带刻度" loading="lazy"></iframe></div>
<details class="m-details"><summary>查看代码(演示页原文 · 20 行)</summary>
<div class="m-code"><div class="m-code-head"><span>frameworks-mobile/Slider.html · steps</span><button class="m-copy" type="button" data-copy="code-demo-2">复制</button></div>
<pre id="code-demo-2"><code>&lt;section class=&quot;demo-block&quot; data-demo=&quot;steps&quot;&gt;
&lt;p class=&quot;demo-label&quot;&gt;带刻度(marks=with-marks:轨道上标出等分点)&lt;/p&gt;
&lt;div class=&quot;demo-pad&quot;&gt;
&lt;div class=&quot;kole-m-slider kole-m-slider--marks&quot; id=&quot;sl-marks&quot; data-min=&quot;0&quot; data-max=&quot;100&quot; data-step=&quot;1&quot; data-value=&quot;60&quot;
data-assert=&quot;slider-marks&quot;&gt;
&lt;span class=&quot;kole-m-slider__rail&quot; role=&quot;slider&quot; tabindex=&quot;0&quot;
aria-label=&quot;亮度&quot; aria-valuemin=&quot;0&quot; aria-valuemax=&quot;100&quot; aria-valuenow=&quot;60&quot;&gt;
&lt;span class=&quot;kole-m-slider__track&quot;&gt;
&lt;span class=&quot;kole-m-slider__filled&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;kole-m-slider__mark&quot; style=&quot;left: 25%&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;kole-m-slider__mark&quot; style=&quot;left: 50%&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;kole-m-slider__mark&quot; style=&quot;left: 75%&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;kole-m-slider__thumb&quot; data-value=&quot;60&quot;&gt;&lt;/span&gt;
&lt;/span&gt;
&lt;/span&gt;
&lt;span class=&quot;kole-m-slider__value&quot;&gt;60&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p class=&quot;demo-note&quot;&gt;刻度只是位置提示;取值仍按 data-step 吸附(默认 1),不会停在刻度之间。&lt;/p&gt;
&lt;/section&gt;</code></pre></div>
</details>
</div>
<div class="m-demo" id="demo-range">
<div class="m-demo-head"><span class="m-demo-title">双滑块区间</span><span class="m-chip">mode=range</span></div>
<p class="m-demo-desc">mode=range:两个滑块各自独立拖动,左值不会越过右值。</p>
<div class="m-demo-stage"><iframe src="../../../frameworks-mobile/Slider.html?demo=range" title="滑动选择器 Slider · 双滑块区间" loading="lazy"></iframe></div>
<details class="m-details"><summary>查看代码(演示页原文 · 17 行)</summary>
<div class="m-code"><div class="m-code-head"><span>frameworks-mobile/Slider.html · range</span><button class="m-copy" type="button" data-copy="code-demo-3">复制</button></div>
<pre id="code-demo-3"><code>&lt;section class=&quot;demo-block&quot; data-demo=&quot;range&quot;&gt;
&lt;p class=&quot;demo-label&quot;&gt;双滑块(mode=range:两个滑块各自独立拖动,左值不会越过右值)&lt;/p&gt;
&lt;div class=&quot;demo-pad&quot;&gt;
&lt;div class=&quot;kole-m-slider kole-m-slider--range&quot; id=&quot;sl-range&quot; data-min=&quot;0&quot; data-max=&quot;100&quot; data-step=&quot;1&quot;
data-value=&quot;20&quot; data-value-max=&quot;70&quot; data-assert=&quot;slider-range&quot;&gt;
&lt;span class=&quot;kole-m-slider__rail&quot; role=&quot;group&quot; aria-label=&quot;价格区间&quot; tabindex=&quot;0&quot;&gt;
&lt;span class=&quot;kole-m-slider__track&quot;&gt;
&lt;span class=&quot;kole-m-slider__filled&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;kole-m-slider__thumb&quot; data-value=&quot;20&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;kole-m-slider__thumb&quot; data-value=&quot;70&quot;&gt;&lt;/span&gt;
&lt;/span&gt;
&lt;/span&gt;
&lt;span class=&quot;kole-m-slider__value&quot;&gt;20~70&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p class=&quot;demo-note&quot;&gt;两个滑块各有自己的 aria-valuenow;拖动时按「离手指更近」判定改哪一个。&lt;/p&gt;
&lt;/section&gt;</code></pre></div>
</details>
</div>
<h3 class="m-demo-group">02 组件状态</h3>
<div class="m-demo" id="demo-dragging">
<div class="m-demo-head"><span class="m-demo-title">拖动中</span><span class="m-chip">状态 dragging</span></div>
<p class="m-demo-desc">滑块由 20px 放大到 24px,并给轨道加一层浅底作「抓住了」的反馈。</p>
<div class="m-demo-stage"><iframe src="../../../frameworks-mobile/Slider.html?demo=dragging" title="滑动选择器 Slider · 拖动中" loading="lazy"></iframe></div>
<details class="m-details"><summary>查看代码(演示页原文 · 16 行)</summary>
<div class="m-code"><div class="m-code-head"><span>frameworks-mobile/Slider.html · dragging</span><button class="m-copy" type="button" data-copy="code-demo-4">复制</button></div>
<pre id="code-demo-4"><code>&lt;section class=&quot;demo-block&quot; data-demo=&quot;dragging&quot;&gt;
&lt;p class=&quot;demo-label&quot;&gt;拖动中(滑块由 20px 放大到 24px,同时给轨道加一层浅底作「抓住了」的反馈)&lt;/p&gt;
&lt;div class=&quot;demo-pad&quot;&gt;
&lt;div class=&quot;kole-m-slider is-dragging&quot; data-min=&quot;0&quot; data-max=&quot;100&quot; data-step=&quot;1&quot; data-value=&quot;75&quot;
data-assert=&quot;slider-dragging&quot;&gt;
&lt;span class=&quot;kole-m-slider__rail&quot; role=&quot;slider&quot; tabindex=&quot;0&quot;
aria-label=&quot;音量&quot; aria-valuemin=&quot;0&quot; aria-valuemax=&quot;100&quot; aria-valuenow=&quot;75&quot;&gt;
&lt;span class=&quot;kole-m-slider__track&quot;&gt;
&lt;span class=&quot;kole-m-slider__filled&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;kole-m-slider__thumb&quot; data-value=&quot;75&quot;&gt;&lt;/span&gt;
&lt;/span&gt;
&lt;/span&gt;
&lt;span class=&quot;kole-m-slider__value&quot;&gt;75&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/section&gt;</code></pre></div>
</details>
</div>
<div class="m-demo" id="demo-disabled">
<div class="m-demo-head"><span class="m-demo-title">禁用</span><span class="m-chip">disabled=true</span></div>
<p class="m-demo-desc">整条置灰且不响应:读屏播报不可用,指针事件不再改值。</p>
<div class="m-demo-stage"><iframe src="../../../frameworks-mobile/Slider.html?demo=disabled" title="滑动选择器 Slider · 禁用" loading="lazy"></iframe></div>
<details class="m-details"><summary>查看代码(演示页原文 · 16 行)</summary>
<div class="m-code"><div class="m-code-head"><span>frameworks-mobile/Slider.html · disabled</span><button class="m-copy" type="button" data-copy="code-demo-5">复制</button></div>
<pre id="code-demo-5"><code>&lt;section class=&quot;demo-block&quot; data-demo=&quot;disabled&quot;&gt;
&lt;p class=&quot;demo-label&quot;&gt;禁用(整条置灰且不响应:读屏会播报不可用,指针事件不再改值)&lt;/p&gt;
&lt;div class=&quot;demo-pad&quot;&gt;
&lt;div class=&quot;kole-m-slider is-disabled&quot; data-min=&quot;0&quot; data-max=&quot;100&quot; data-value=&quot;30&quot;
aria-disabled=&quot;true&quot; data-assert=&quot;slider-disabled&quot;&gt;
&lt;span class=&quot;kole-m-slider__rail&quot; role=&quot;slider&quot; aria-label=&quot;热度权重&quot;
aria-valuemin=&quot;0&quot; aria-valuemax=&quot;100&quot; aria-valuenow=&quot;30&quot; aria-disabled=&quot;true&quot;&gt;
&lt;span class=&quot;kole-m-slider__track&quot;&gt;
&lt;span class=&quot;kole-m-slider__filled&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;kole-m-slider__thumb&quot; data-value=&quot;30&quot;&gt;&lt;/span&gt;
&lt;/span&gt;
&lt;/span&gt;
&lt;span class=&quot;kole-m-slider__value&quot;&gt;30&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/section&gt;</code></pre></div>
</details>
</div>
</section>
<section class="m-block">
<h2 id="api">API</h2>
<p class="m-sub">props / events / slots 为 6 端实现的公共接口(说明文字取自规格对应小节)。字段名与各端源码逐名核对:node tools/verify-mobile-docs.mjs</p>
<h3 class="m-h3">Props</h3>
<table class="m-table">
<thead><tr><th>名称</th><th>类型</th><th>默认值</th><th>说明</th><th>必传</th></tr></thead>
<tbody>
<tr><td><code>value</code></td><td><code>number</code></td><td><code>0</code></td><td>受控当前值(range 模式下是左值),与 aria-valuenow 同步写(规格 §35.5)</td><td>N</td></tr>
<tr><td><code>valueMax</code></td><td><code>number</code></td><td><code>100</code></td><td>mode=range 时的右值(single 模式忽略)(规格 §35.3)</td><td>N</td></tr>
<tr><td><code>min</code></td><td><code>number</code></td><td><code>0</code></td><td>区间下限,拖动与键盘步进都不会低于它(规格 §35.5)</td><td>N</td></tr>
<tr><td><code>max</code></td><td><code>number</code></td><td><code>100</code></td><td>区间上限,拖动与键盘步进都不会高于它(规格 §35.5)</td><td>N</td></tr>
<tr><td><code>step</code></td><td><code>number</code></td><td><code>1</code></td><td>取值吸附步长;也是双滑块的最小间距与键盘步进量(规格 §35.5)</td><td>N</td></tr>
<tr><td><code>mode</code></td><td><code>'single' | 'range'</code></td><td><code>'single'</code></td><td>变体 mode:单滑块还是双滑块区间(规格 §35.3)</td><td>N</td></tr>
<tr><td><code>marks</code></td><td><code>boolean</code></td><td><code>false</code></td><td>变体 marks:轨道上是否标出等分刻度点(规格 §35.3)</td><td>N</td></tr>
<tr><td><code>showValue</code></td><td><code>boolean</code></td><td><code>true</code></td><td>变体 showValue:是否在轨道右侧显示当前值(规格 §35.3)</td><td>N</td></tr>
<tr><td><code>disabled</code></td><td><code>boolean</code></td><td><code>false</code></td><td>状态 disabled:整条置灰且不响应,指针与键盘都不改值(规格 §35.4)</td><td>N</td></tr>
<tr><td><code>label</code></td><td><code>string</code></td><td><code>'滑动选择'</code></td><td>无障碍名称,落到 role=&quot;slider&quot; 的 aria-label(规格 §35.6)</td><td>N</td></tr>
</tbody>
</table>
<p class="m-note">「必传」按严格定义:实现里**没有默认值**时才为 Y(本门禁逐条核对 props 与各端源码的默认值,防止契约与实现脱节)。</p>
<h3 class="m-h3">事件</h3>
<table class="m-table">
<thead><tr><th>名称</th><th>参数</th><th>说明</th></tr></thead>
<tbody>
<tr><td><code>change</code></td><td><code>(value)</code></td><td>拖动或键盘步进改值时触发,连续回传当前值(range 模式回传 [左值, 右值])(规格 §35.5)</td></tr>
</tbody>
</table>
<h3 class="m-h3">插槽</h3>
<table class="m-table">
<thead><tr><th>名称</th><th>说明</th></tr></thead>
<tbody>
<tr><td><code>default</code></td><td>数值之后的自定义内容(如单位文案,规格 §35.2 slider)</td></tr>
</tbody>
</table>
<h3 class="m-h3">CSS 变量</h3>
<p class="m-sub">组件级变量(在组件样式表里定义)。业务侧可在自己的作用域内覆盖,不必改组件源码。</p>
<table class="m-table">
<thead><tr><th>名称</th><th>默认值</th><th>说明</th></tr></thead>
<tbody>
<tr><td><code>--kole-m-slider-thumb</code></td><td><code>20px</code></td><td>滑块视觉直径</td></tr>
<tr><td><code>--kole-m-slider-thumb-active</code></td><td><code>24px</code></td><td>拖动中的滑块直径</td></tr>
<tr><td><code>--kole-m-slider-track</code></td><td><code>4px</code></td><td>轨道可视高度</td></tr>
</tbody>
</table>
</section>
<section class="m-block">
<h2 id="usage">何时使用</h2>
<ul>
<li>在一个连续区间里用一次滑动选出数值(预算上限、价格区间、音量、亮度)</li>
<li>轨道可视高度只有 4px,但整条轨道所在的行必须是 44px 高的热区</li>
<li>按下任意位置就要跳到该处 —— 触屏上要求用户精确抓住 20px 的滑块是点不中的</li>
<li>取值按 step 吸附并夹在 [min, max];mode=range 时左值不越过右值、右值不越过左值</li>
<li>值变化立即触发 change 事件(连续回传),不做防抖;松手时才做业务提交</li>
</ul>
</section>
<section class="m-block">
<h2 id="interaction">交互与触控</h2>
<ul>
<li>轨道热区高 44px,**按下任意位置即跳到该处**,随后横向拖动连续改值;不要求抓住滑块</li>
<li>手势是「按下 + 拖动」一次连续动作,不响应长按或双击;横向分量优先,纵向滚动不被吞掉(touch-action: pan-y)</li>
<li>取值按 step 吸附并夹在 [min, max];mode=range 时左值不越过右值、右值不越过左值</li>
<li>拖动期间滑块放大到 24px,松手后 150ms 回弹;减少动态偏好下瞬时切换</li>
<li>值变化立即触发 change 事件(连续回传),不做防抖;松手时才做业务提交</li>
</ul>
</section>
<section class="m-block">
<h2 id="a11y">无障碍</h2>
<ul>
<li>单滑块:轨道热区是 role=&quot;slider&quot; 且可聚焦,写 aria-valuemin / aria-valuemax / aria-valuenow / aria-label</li>
<li>双滑块:外层是 role=&quot;group&quot; + aria-label,每个滑块各自持有 aria-valuenow(两条独立数值)</li>
<li>禁用时写 aria-disabled=&quot;true&quot;,读屏会播报不可用</li>
<li>键盘:聚焦后左右方向键按 step 步进(触屏之外的替代路径)</li>
</ul>
</section>
<section class="m-block">
<h2 id="related">相似组件</h2>
<p class="m-sub">从「该用哪一个」的角度区分;PC 端的对应实现见 <a href="../../index.html">PC 文档站</a>。</p>
<table class="m-table">
<thead><tr><th>组件</th><th>何时用它而不是本组件</th></tr></thead>
<tbody>
<tr><td><a href="mobile-stepper.html">步进器</a><em style="font-family: var(--kole-font-family-num); font-style: normal; font-size: 11px; color: var(--kole-color-text-placeholder); margin-left: 6px;">Stepper</em></td><td>有界区间里加减小整数、且需要精确值时用 Stepper;数值连续、更看重「大致位置」时才用 Slider</td></tr>
<tr><td><a href="mobile-input.html">输入框</a><em style="font-family: var(--kole-font-family-num); font-style: normal; font-size: 11px; color: var(--kole-color-text-placeholder); margin-left: 6px;">Input</em></td><td>需要键盘输入精确数值(金额、编号)时用 Input + 数字键盘;Slider 只适合粗选</td></tr>
<tr><td><a href="mobile-form.html">表单</a><em style="font-family: var(--kole-font-family-num); font-style: normal; font-size: 11px; color: var(--kole-color-text-placeholder); margin-left: 6px;">Form</em></td><td>滑块作为表单里的一个字段时,标签、必填星号与错误提示由 Form 的 item 提供</td></tr>
</tbody>
</table>
</section>
<section class="m-block">
<h2 id="gaps">规格未定 / 禁止发明</h2>
<table class="m-table">
<thead><tr><th>类别</th><th>条目</th></tr></thead>
<tbody>
<tr><td><span class="m-chip">禁止发明</span></td><td>数值输入框与滑块的双向绑定(宿主自行组合 Input,本组件只回传值)</td></tr>
<tr><td><span class="m-chip">禁止发明</span></td><td>刻度上的文案标注与区间高亮(只标位置,不写文案)</td></tr>
<tr><td><span class="m-chip">禁止发明</span></td><td>拖动结束后的「撤销 / 重置」流程</td></tr>
<tr><td><span class="m-chip">禁止发明</span></td><td>纵向滑块与圆形表盘式选择器</td></tr>
<tr><td><span class="m-chip">规格未定</span></td><td>双滑块的最小间距是否应当是 1 个 step(当前是),还是用户可配</td></tr>
<tr><td><span class="m-chip">规格未定</span></td><td>刻度数量是否应由 step 推导(当前按 5 等分固定标点)</td></tr>
<tr><td><span class="m-chip">规格未定</span></td><td>拖动中是否要把当前值放大显示在滑块上方(气泡提示)</td></tr>
</tbody>
</table>
</section>
<section class="m-block">
<h2 id="anatomy">结构(anatomy)</h2>
<table class="m-table">
<thead><tr><th>字段</th><th>说明</th></tr></thead>
<tbody>
<tr><td><code>slider</code></td><td>根元素,一行里放进「轨道热区 + 数值」</td></tr>
<tr><td><code>rail</code></td><td>轨道热区,44px 高的可聚焦区域,承载指针事件与锚点</td></tr>
<tr><td><code>track</code></td><td>轨道本体(可视 4px),承载选中段与刻度的边界</td></tr>
<tr><td><code>filled</code></td><td>选中段,从起点铺到滑块位置</td></tr>
<tr><td><code>thumb</code></td><td>滑块,位置由当前值的百分比决定;拖动中放大作为「抓住了」的反馈</td></tr>
<tr><td><code>mark</code></td><td>刻度点,仅 marks=with-marks 时出现(只标位置,不写文案)</td></tr>
</tbody>
</table>
</section>
<section class="m-block">
<h2 id="variants">变体维度与类名映射</h2>
<p class="m-sub">类名映射由构建脚本从契约 <code>variantClasses</code> 生成,并被 <code>verify:mobile-docs</code> 逐条对照组件 CSS 校验(类/变量必须真实存在)。</p>
<table class="m-table">
<thead><tr><th>维度</th><th>取值</th><th>对应类名 / 变量</th></tr></thead>
<tbody>
<tr><td><code>mode</code></td><td>single / range</td><td><div><b>single</b> <span class="m-none">(由数据驱动,无专属类)</span></div><div><b>range</b> <span class="m-cls">.kole-m-slider--range</span></div></td></tr>
<tr><td><code>marks</code></td><td>with-marks / none</td><td><div><b>with-marks</b> <span class="m-cls">.kole-m-slider--marks</span> <span class="m-cls">.kole-m-slider__mark</span></div><div><b>none</b> <span class="m-none">(由数据驱动,无专属类)</span></div></td></tr>
<tr><td><code>showValue</code></td><td>true / false</td><td><div><b>true</b> <span class="m-none">(由数据驱动,无专属类)</span></div><div><b>false</b> <span class="m-none">(由数据驱动,无专属类)</span></div></td></tr>
</tbody>
</table>
</section>
<section class="m-block">
<h2 id="rep-variants">代表变体</h2>
<table class="m-table">
<thead><tr><th>变体</th><th>标签</th></tr></thead>
<tbody>
<tr><td><code>mode=single · marks=none · showValue=true</code></td><td>默认(单滑块 + 右侧数值)</td></tr>
<tr><td><code>mode=single · marks=with-marks · showValue=true</code></td><td>带刻度(轨道上标出等分点)</td></tr>
<tr><td><code>mode=range · marks=none · showValue=true</code></td><td>双滑块区间(左值不越过右值)</td></tr>
</tbody>
</table>
</section>
<section class="m-block">
<h2 id="tokens">用到的令牌</h2>
<p class="m-sub">构建时从本组件样式表扫描得出。蓝色为移动端自有令牌,绿色为继承的 PC 令牌(改一处两端生效)。</p>
<div class="m-chips">
<span class="m-chip m" title="正文 / 列表项">--kole-m-font-size-body</span>
<span class="m-chip m" title="次级说明">--kole-m-font-size-label</span>
<span class="m-chip m" title="页面左右留白">--kole-m-gutter</span>
<span class="m-chip m" title="可点区域最小边长">--kole-m-touch-target</span>
<span class="m-chip inherit" title="分割线/边框">--kole-color-border</span>
<span class="m-chip inherit" title="品牌主色:主按钮、选中态、链接">--kole-color-brand</span>
<span class="m-chip inherit" title="品牌浅色背景:选中背景、标签背景、表格悬停">--kole-color-brand-bg</span>
<span class="m-chip inherit" title="卡片背景">--kole-color-card-bg</span>
<span class="m-chip inherit" title="禁用背景(按钮禁用态)">--kole-color-disabled-bg</span>
<span class="m-chip inherit" title="输入框聚焦外发光">--kole-color-focus-ring</span>
<span class="m-chip inherit" title="正文文字 · 14.0:1">--kole-color-text-body</span>
<span class="m-chip inherit" title="禁用态文字 · WCAG 1.4.3 对禁用控件豁免(规范定义于组件1/2.txt)">--kole-color-text-disabled</span>
<span class="m-chip inherit" title="占位文字 · 4.54:1(原 #BFBFBF 仅 1.84:1)">--kole-color-text-placeholder</span>
<span class="m-chip inherit" title="次要文字 · 5.10:1(原 #8C8C8C 仅 3.36:1)">--kole-color-text-secondary</span>
<span class="m-chip inherit" title="">--kole-ease-standard</span>
<span class="m-chip inherit" title="卡片默认">--kole-shadow-low</span>
<span class="m-chip inherit" title="">--kole-space-12</span>
<span class="m-chip" title="组件级变量(可在业务侧覆盖)">--kole-m-slider-thumb</span>
<span class="m-chip" title="组件级变量(可在业务侧覆盖)">--kole-m-slider-thumb-active</span>
<span class="m-chip" title="组件级变量(可在业务侧覆盖)">--kole-m-slider-track</span>
</div>
</section>
<section class="m-block">
<h2 id="sources">6 端源码</h2>
<p class="m-sub">同一组件的六份实现(生产环境的类名与结构一致,差异只在技术栈写法与单位)。点开查看,右侧可复制。</p>
<details class="m-details">
<summary>frameworks-mobile/Slider.css · 纯样式(CSS) · 135 行</summary>
<div class="m-code">
<div class="m-code-head"><span>frameworks-mobile/Slider.css</span><button class="m-copy" type="button" data-copy="code-211">复制</button></div>
<pre id="code-211"><code>/* Kole UI Mobile · Slider 样式 — 对齐移动端规格 §35
滑动选择器:轨道可视高度只有 4px,但**整条热区高度是 44px**(触控最小边长)——
直接点轨道上的任意位置也要能跳到该位置,不能要求用户精确抓住滑块。
滑块(thumb)视觉 20px,按下/拖动时放大到 24px 作为「手指抓住了」的反馈。 */
.kole-m-slider {
/* 组件级变量:业务侧可在容器上覆盖 */
--kole-m-slider-thumb: 20px; /* 滑块视觉直径 */
--kole-m-slider-thumb-active: 24px; /* 拖动中的滑块直径 */
--kole-m-slider-track: 4px; /* 轨道可视高度 */
box-sizing: border-box;
display: flex;
align-items: center;
gap: var(--kole-space-12);
width: 100%;
padding: 0 var(--kole-m-gutter);
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.4;
/* 注意:根元素**不写** cursor: pointer —— 数值文案是显示而不是控件,
整条挂 pointer 会让它看起来可点(键盘可达性判据也会因此报「鼠标专用交互」)。
可点的语义由 .kole-m-slider__rail(role=&quot;slider&quot; + tabindex)承担。 */
cursor: default;
touch-action: pan-y;
}
/* 轨道热区:可视 4px 的轨道垂直居中在这条 44px 高的带子里 */
.kole-m-slider__rail {
position: relative;
flex: 1 1 auto;
min-width: 0;
box-sizing: border-box;
display: flex;
align-items: center;
min-height: var(--kole-m-touch-target);
cursor: pointer;
}
.kole-m-slider__track {
position: relative;
box-sizing: border-box;
width: 100%;
height: var(--kole-m-slider-track);
border-radius: calc(var(--kole-m-slider-track) / 2);
background: var(--kole-color-border);
}
/* 选中段:从起点铺到滑块位置(宽度由 left/right 内联样式给出) */
.kole-m-slider__filled {
position: absolute;
top: 0;
bottom: 0;
left: 0;
border-radius: inherit;
background: var(--kole-color-brand);
}
/* 滑块:白底 + 品牌描边 + 投影,保证在浅色轨道上也看得清 */
.kole-m-slider__thumb {
position: absolute;
top: 50%;
box-sizing: border-box;
width: var(--kole-m-slider-thumb);
height: var(--kole-m-slider-thumb);
margin-top: calc(var(--kole-m-slider-thumb) / -2);
margin-left: calc(var(--kole-m-slider-thumb) / -2);
border: 2px solid var(--kole-color-brand);
border-radius: 50%;
background: var(--kole-color-card-bg);
box-shadow: var(--kole-shadow-low);
transition: width 150ms var(--kole-ease-standard), height 150ms var(--kole-ease-standard),
margin 150ms var(--kole-ease-standard);
}
/* 状态 dragging:滑块放大(手指抓住的反馈),并在整条轨道上加一层浅底 */
.kole-m-slider.is-dragging .kole-m-slider__thumb {
width: var(--kole-m-slider-thumb-active);
height: var(--kole-m-slider-thumb-active);
margin-top: calc(var(--kole-m-slider-thumb-active) / -2);
margin-left: calc(var(--kole-m-slider-thumb-active) / -2);
}
.kole-m-slider.is-dragging .kole-m-slider__rail { background: var(--kole-color-brand-bg); }
/* 双滑块(mode=range):两个滑块共用同一个类,位置由内联 left 区分 */
.kole-m-slider--range .kole-m-slider__thumb { background: var(--kole-color-card-bg); }
/* 数值显示:当前值跟在轨道右侧(宽度固定,避免拖动时文字宽度变化导致轨道抖动) */
.kole-m-slider__value {
flex: 0 0 auto;
min-width: 3em;
text-align: end;
font-variant-numeric: tabular-nums;
color: var(--kole-color-text-secondary);
font-size: var(--kole-m-font-size-label);
}
/* 变体 marks=with-marks:轨道上出现刻度点(只标位置,不写文案,避免小屏拥挤)。
默认不渲染刻度点 —— 刻度是「有意义的等分点」而非装饰,没有它时轨道更干净。 */
.kole-m-slider__mark {
display: none;
position: absolute;
top: 50%;
width: 2px;
height: 8px;
margin-top: -4px;
margin-left: -1px;
border-radius: 1px;
background: var(--kole-color-text-placeholder);
}
.kole-m-slider--marks .kole-m-slider__mark { display: block; }
/* 状态 disabled:整条置灰且不响应(滑块不再有描边) */
.kole-m-slider.is-disabled {
cursor: not-allowed;
color: var(--kole-color-text-disabled);
}
.kole-m-slider.is-disabled .kole-m-slider__thumb {
border-color: var(--kole-color-text-disabled);
background: var(--kole-color-disabled-bg);
}
.kole-m-slider.is-disabled .kole-m-slider__filled { background: var(--kole-color-text-disabled); }
.kole-m-slider.is-disabled .kole-m-slider__value { color: var(--kole-color-text-disabled); }
.kole-m-slider:focus-visible {
outline: 2px solid var(--kole-color-focus-ring);
outline-offset: -2px;
}
</code></pre>
</div>
</details>
<details class="m-details">
<summary>frameworks-mobile/Slider.html · H5 原生(无框架) · 254 行</summary>
<div class="m-code">
<div class="m-code-head"><span>frameworks-mobile/Slider.html</span><button class="m-copy" type="button" data-copy="code-212">复制</button></div>
<pre id="code-212"><code>&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;zh-CN&quot;&gt;
&lt;head&gt;
&lt;meta charset=&quot;UTF-8&quot;&gt;
&lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0, viewport-fit=cover&quot;&gt;
&lt;title&gt;Kole UI Mobile · Slider(H5)&lt;/title&gt;
&lt;link rel=&quot;stylesheet&quot; href=&quot;../.design_library/kole-ui-mobile/colors_and_type.css&quot;&gt;
&lt;link rel=&quot;stylesheet&quot; href=&quot;Slider.css&quot;&gt;
&lt;style&gt;
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-pad { padding-bottom: var(--kole-space-16); background: var(--kole-color-card-bg); border-block: 1px solid var(--kole-color-border); }
.demo-note { margin: 0; padding: 0 var(--kole-m-gutter) var(--kole-space-12);
font-size: var(--kole-m-font-size-label); color: var(--kole-color-text-secondary); }
.demo-block[hidden] { display: none; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;div class=&quot;demo&quot;&gt;
&lt;section class=&quot;demo-block&quot; data-demo=&quot;basic&quot;&gt;
&lt;p class=&quot;demo-label&quot;&gt;基础用法(在轨道上按住拖动即改值;轨道可视 4px、热区 44px 高)&lt;/p&gt;
&lt;div class=&quot;demo-pad&quot;&gt;
&lt;div class=&quot;kole-m-slider&quot; id=&quot;sl-basic&quot; data-min=&quot;0&quot; data-max=&quot;100&quot; data-step=&quot;1&quot; data-value=&quot;40&quot;
data-assert=&quot;slider-basic&quot; data-behavior=&quot;swipe-sets-class:#sl-basic|is-dragging&quot;&gt;
&lt;span class=&quot;kole-m-slider__rail&quot; role=&quot;slider&quot; tabindex=&quot;0&quot;
aria-label=&quot;预算上限&quot; aria-valuemin=&quot;0&quot; aria-valuemax=&quot;100&quot; aria-valuenow=&quot;40&quot;&gt;
&lt;span class=&quot;kole-m-slider__track&quot;&gt;
&lt;span class=&quot;kole-m-slider__filled&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;kole-m-slider__thumb&quot; data-value=&quot;40&quot;&gt;&lt;/span&gt;
&lt;/span&gt;
&lt;/span&gt;
&lt;span class=&quot;kole-m-slider__value&quot;&gt;40&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p class=&quot;demo-note&quot;&gt;在轨道上按住即跳到手指处,横向拖动连续改值;松手后滑块 150ms 回弹到常态尺寸。&lt;/p&gt;
&lt;/section&gt;
&lt;section class=&quot;demo-block&quot; data-demo=&quot;steps&quot;&gt;
&lt;p class=&quot;demo-label&quot;&gt;带刻度(marks=with-marks:轨道上标出等分点)&lt;/p&gt;
&lt;div class=&quot;demo-pad&quot;&gt;
&lt;div class=&quot;kole-m-slider kole-m-slider--marks&quot; id=&quot;sl-marks&quot; data-min=&quot;0&quot; data-max=&quot;100&quot; data-step=&quot;1&quot; data-value=&quot;60&quot;
data-assert=&quot;slider-marks&quot;&gt;
&lt;span class=&quot;kole-m-slider__rail&quot; role=&quot;slider&quot; tabindex=&quot;0&quot;
aria-label=&quot;亮度&quot; aria-valuemin=&quot;0&quot; aria-valuemax=&quot;100&quot; aria-valuenow=&quot;60&quot;&gt;
&lt;span class=&quot;kole-m-slider__track&quot;&gt;
&lt;span class=&quot;kole-m-slider__filled&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;kole-m-slider__mark&quot; style=&quot;left: 25%&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;kole-m-slider__mark&quot; style=&quot;left: 50%&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;kole-m-slider__mark&quot; style=&quot;left: 75%&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;kole-m-slider__thumb&quot; data-value=&quot;60&quot;&gt;&lt;/span&gt;
&lt;/span&gt;
&lt;/span&gt;
&lt;span class=&quot;kole-m-slider__value&quot;&gt;60&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p class=&quot;demo-note&quot;&gt;刻度只是位置提示;取值仍按 data-step 吸附(默认 1),不会停在刻度之间。&lt;/p&gt;
&lt;/section&gt;
&lt;section class=&quot;demo-block&quot; data-demo=&quot;range&quot;&gt;
&lt;p class=&quot;demo-label&quot;&gt;双滑块(mode=range:两个滑块各自独立拖动,左值不会越过右值)&lt;/p&gt;
&lt;div class=&quot;demo-pad&quot;&gt;
&lt;div class=&quot;kole-m-slider kole-m-slider--range&quot; id=&quot;sl-range&quot; data-min=&quot;0&quot; data-max=&quot;100&quot; data-step=&quot;1&quot;
data-value=&quot;20&quot; data-value-max=&quot;70&quot; data-assert=&quot;slider-range&quot;&gt;
&lt;span class=&quot;kole-m-slider__rail&quot; role=&quot;group&quot; aria-label=&quot;价格区间&quot; tabindex=&quot;0&quot;&gt;
&lt;span class=&quot;kole-m-slider__track&quot;&gt;
&lt;span class=&quot;kole-m-slider__filled&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;kole-m-slider__thumb&quot; data-value=&quot;20&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;kole-m-slider__thumb&quot; data-value=&quot;70&quot;&gt;&lt;/span&gt;
&lt;/span&gt;
&lt;/span&gt;
&lt;span class=&quot;kole-m-slider__value&quot;&gt;20~70&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p class=&quot;demo-note&quot;&gt;两个滑块各有自己的 aria-valuenow;拖动时按「离手指更近」判定改哪一个。&lt;/p&gt;
&lt;/section&gt;
&lt;section class=&quot;demo-block&quot; data-demo=&quot;dragging&quot;&gt;
&lt;p class=&quot;demo-label&quot;&gt;拖动中(滑块由 20px 放大到 24px,同时给轨道加一层浅底作「抓住了」的反馈)&lt;/p&gt;
&lt;div class=&quot;demo-pad&quot;&gt;
&lt;div class=&quot;kole-m-slider is-dragging&quot; data-min=&quot;0&quot; data-max=&quot;100&quot; data-step=&quot;1&quot; data-value=&quot;75&quot;
data-assert=&quot;slider-dragging&quot;&gt;
&lt;span class=&quot;kole-m-slider__rail&quot; role=&quot;slider&quot; tabindex=&quot;0&quot;
aria-label=&quot;音量&quot; aria-valuemin=&quot;0&quot; aria-valuemax=&quot;100&quot; aria-valuenow=&quot;75&quot;&gt;
&lt;span class=&quot;kole-m-slider__track&quot;&gt;
&lt;span class=&quot;kole-m-slider__filled&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;kole-m-slider__thumb&quot; data-value=&quot;75&quot;&gt;&lt;/span&gt;
&lt;/span&gt;
&lt;/span&gt;
&lt;span class=&quot;kole-m-slider__value&quot;&gt;75&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/section&gt;
&lt;section class=&quot;demo-block&quot; data-demo=&quot;disabled&quot;&gt;
&lt;p class=&quot;demo-label&quot;&gt;禁用(整条置灰且不响应:读屏会播报不可用,指针事件不再改值)&lt;/p&gt;
&lt;div class=&quot;demo-pad&quot;&gt;
&lt;div class=&quot;kole-m-slider is-disabled&quot; data-min=&quot;0&quot; data-max=&quot;100&quot; data-value=&quot;30&quot;
aria-disabled=&quot;true&quot; data-assert=&quot;slider-disabled&quot;&gt;
&lt;span class=&quot;kole-m-slider__rail&quot; role=&quot;slider&quot; aria-label=&quot;热度权重&quot;
aria-valuemin=&quot;0&quot; aria-valuemax=&quot;100&quot; aria-valuenow=&quot;30&quot; aria-disabled=&quot;true&quot;&gt;
&lt;span class=&quot;kole-m-slider__track&quot;&gt;
&lt;span class=&quot;kole-m-slider__filled&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;kole-m-slider__thumb&quot; data-value=&quot;30&quot;&gt;&lt;/span&gt;
&lt;/span&gt;
&lt;/span&gt;
&lt;span class=&quot;kole-m-slider__value&quot;&gt;30&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/section&gt;
&lt;/div&gt;
&lt;script&gt;
/* 演示页脚本:真实拖动。
- 浏览器端用 Pointer Events:pointerdown 落在轨道热区上即跳到该处,pointermove 跟随
- 值按 data-step(默认 1)吸附并夹在 [min, max];双滑块时左值不越过右值
- 拖动期间(含松手后 150ms 回弹)根元素带 is-dragging
uni-app 端同一手势用 @touchstart/@touchmove/@touchend 实现(小程序没有 PointerEvent)。
真实业务里这份状态由宿主管理(受控 value + change 事件),此处是最小可运行实现。 */
(function () {
/* 松手后滑块以 150ms 回弹到常态尺寸,这段过渡期间保持 is-dragging(避免过渡被打断) */
var REBOUND = 150;
Array.prototype.forEach.call(document.querySelectorAll('.kole-m-slider'), function (root) {
if (root.classList.contains('is-disabled') || root.getAttribute('aria-disabled') === 'true') return;
var rail = root.querySelector('.kole-m-slider__rail');
var track = root.querySelector('.kole-m-slider__track');
var filled = root.querySelector('.kole-m-slider__filled');
var valueEl = root.querySelector('.kole-m-slider__value');
var thumbs = Array.prototype.slice.call(root.querySelectorAll('.kole-m-slider__thumb'));
if (!rail || !track || !filled || !thumbs.length) return;
var min = Number(root.getAttribute('data-min') || 0);
var max = Number(root.getAttribute('data-max') || 100);
var step = Number(root.getAttribute('data-step') || 1);
var range = thumbs.length &gt; 1;
var active = -1;
var timer = null;
function clamp(v) {
var snapped = Math.round((v - min) / step) * step + min;
return Math.max(min, Math.min(max, snapped));
}
function ratio(v) { return max === min ? 0 : (v - min) / (max - min); }
function valueAt(clientX) {
var rect = track.getBoundingClientRect();
if (!rect.width) return min;
return clamp(min + ((clientX - rect.left) / rect.width) * (max - min));
}
function values() {
return thumbs.map(function (t) { return Number(t.getAttribute('data-value') || min); });
}
function render() {
var v = values();
thumbs.forEach(function (t, i) { t.style.left = (ratio(v[i]) * 100) + '%'; });
if (range) {
filled.style.left = (ratio(v[0]) * 100) + '%';
filled.style.right = (100 - ratio(v[1]) * 100) + '%';
if (valueEl) valueEl.textContent = v[0] + '~' + v[1];
root.setAttribute('data-value', String(v[0]));
root.setAttribute('data-value-max', String(v[1]));
thumbs[0].setAttribute('aria-valuenow', String(v[0]));
thumbs[1].setAttribute('aria-valuenow', String(v[1]));
} else {
filled.style.right = (100 - ratio(v[0]) * 100) + '%';
if (valueEl) valueEl.textContent = String(v[0]);
root.setAttribute('data-value', String(v[0]));
rail.setAttribute('aria-valuenow', String(v[0]));
}
}
function nearest(v) {
if (!range) return 0;
var cur = values();
return Math.abs(cur[0] - v) &lt;= Math.abs(cur[1] - v) ? 0 : 1;
}
function setValue(index, v) {
/* 双滑块:左值不越过右值、右值不越过左值(各留一个 step 的间距) */
if (range) {
var other = values()[1 - index];
v = index === 0 ? Math.min(v, other - step) : Math.max(v, other + step);
}
thumbs[index].setAttribute('data-value', String(clamp(v)));
render();
}
function start(e) {
/* 值区只显示数字、不接受拖动(它是显示而不是控件) */
if (e.target &amp;&amp; e.target.closest &amp;&amp; e.target.closest('.kole-m-slider__value')) return;
var v = valueAt(e.clientX);
active = nearest(v);
if (timer) { clearTimeout(timer); timer = null; }
root.classList.add('is-dragging');
setValue(active, v);
if (root.setPointerCapture) { try { root.setPointerCapture(e.pointerId); } catch (err) { /* 忽略 */ } }
}
function move(e) {
if (active &lt; 0) return;
setValue(active, valueAt(e.clientX));
}
function end() {
if (active &lt; 0) return;
active = -1;
timer = setTimeout(function () {
root.classList.remove('is-dragging');
timer = null;
}, REBOUND);
}
/* 手势挂在根元素上:整个控件(含轨道两侧留白)都是有效起点,符合「44px 热区」的意图;
行为断言库的 swipe-sets-class 也把事件派发在根元素上(同一契约)。 */
root.addEventListener('pointerdown', start);
root.addEventListener('pointermove', move);
root.addEventListener('pointerup', end);
root.addEventListener('pointercancel', end);
/* 键盘:单滑块的左右方向键按 step 步进(滑条热区可聚焦,靠 tabindex 进键盘序列) */
root.addEventListener('keydown', function (e) {
if (range) return;
var delta = e.key === 'ArrowLeft' || e.key === 'ArrowDown' ? -step
: e.key === 'ArrowRight' || e.key === 'ArrowUp' ? step : 0;
if (!delta) return;
e.preventDefault();
setValue(0, values()[0] + delta);
});
/* 初始值:双滑块从两根 thumb 的 data-value 读,单滑块从根节点的 data-value 读 */
if (!range) thumbs[0].setAttribute('data-value', root.getAttribute('data-value') || String(min));
render();
});
})();
&lt;/script&gt;
&lt;script&gt;
/* ?demo=&lt;id&gt; → 只显示该演示块(文档站按块预览用;无参数时全部显示,测试与回归走无参数路径) */
(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 &amp;&amp; !b.hidden) label.hidden = true;
});
})();
&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
</div>
</details>
<details class="m-details">
<summary>frameworks-mobile/Slider.jsx · React · 145 行</summary>
<div class="m-code">
<div class="m-code-head"><span>frameworks-mobile/Slider.jsx</span><button class="m-copy" type="button" data-copy="code-213">复制</button></div>
<pre id="code-213"><code>import React, { useRef, useState } from 'react';
import './Slider.css';
/* 滑动选择器(移动端)— 规格 §35
轨道可视高度只有 4px,但**整条热区高度是 44px**:直接点轨道任意位置也要能跳到该处。
浏览器端手势用 Pointer Events(pointerdown / pointermove / pointerup + setPointerCapture);
uni-app 端同一手势改用 @touch*(小程序与 App 端没有 PointerEvent)。
值按 step 吸附并夹在 [min, max];mode=range 时左值不越过右值。
受控:本端不存储值,只回传目标值 onChange,是否采纳由宿主决定。 */
export default function Slider({
value = 0,
valueMax = 100,
min = 0,
max = 100,
step = 1,
mode = 'single',
marks = false,
showValue = true,
disabled = false,
label = '滑动选择',
onChange,
children = null,
}) {
const railRef = useRef(null);
const [dragging, setDragging] = useState(false);
const [active, setActive] = useState(-1);
/* 双滑块的两个值;单滑块只用到第一个 */
const [inner, setInner] = useState(null);
const values = inner || (mode === 'range' ? [value, valueMax] : [value]);
const range = mode === 'range';
const cls =
'kole-m-slider' +
(range ? ' kole-m-slider--range' : '') +
(marks ? ' kole-m-slider--marks' : '') +
(dragging ? ' is-dragging' : '') +
(disabled ? ' is-disabled' : '');
const ratio = (v) =&gt; (max === min ? 0 : (v - min) / (max - min));
const clamp = (v) =&gt; {
const snapped = Math.round((v - min) / step) * step + min;
return Math.max(min, Math.min(max, snapped));
};
function valueAt(clientX) {
const el = railRef.current;
if (!el) return min;
const rect = el.getBoundingClientRect();
if (!rect.width) return min;
return clamp(min + ((clientX - rect.left) / rect.width) * (max - min));
}
function nearest(v) {
if (!range) return 0;
return Math.abs(values[0] - v) &lt;= Math.abs(values[1] - v) ? 0 : 1;
}
function commit(index, v) {
/* 双滑块:左值不越过右值、右值不越过左值(各留一个 step 的间距) */
let next = clamp(v);
if (range) {
const other = values[1 - index];
next = index === 0 ? Math.min(next, other - step) : Math.max(next, other + step);
}
const out = range ? [values[0], values[1]] : [next];
out[index] = next;
setInner(out);
if (onChange) onChange(range ? out : out[0]);
}
/* 手势起止都放在根元素上:整个控件(含两侧留白)都是有效起点 */
function onPointerDown(e) {
if (disabled) return;
if (e.target &amp;&amp; e.target.closest &amp;&amp; e.target.closest('.kole-m-slider__value')) return;
const v = valueAt(e.clientX);
setActive(nearest(v));
setDragging(true);
commit(nearest(v), v);
const el = railRef.current;
if (el &amp;&amp; el.setPointerCapture) {
try {
el.setPointerCapture(e.pointerId);
} catch (err) {
/* 指针捕获不可用时靠冒泡事件继续跟随 */
}
}
}
function onPointerMove(e) {
if (active &lt; 0) return;
commit(active, valueAt(e.clientX));
}
function onPointerUp() {
if (active &lt; 0) return;
setActive(-1);
setDragging(false);
}
function onKeyDown(e) {
if (disabled || range) return;
const delta =
e.key === 'ArrowLeft' || e.key === 'ArrowDown' ? -step : e.key === 'ArrowRight' || e.key === 'ArrowUp' ? step : 0;
if (!delta) return;
e.preventDefault();
commit(0, values[0] + delta);
}
return (
&lt;div
className={cls}
onPointerDown={onPointerDown}
onPointerMove={onPointerMove}
onPointerUp={onPointerUp}
onPointerCancel={onPointerUp}
onKeyDown={onKeyDown}
&gt;
&lt;span className=&quot;kole-m-slider__rail&quot; role={range ? 'group' : 'slider'} tabIndex={disabled ? undefined : 0} ref={railRef} aria-label={label} aria-valuemin={min} aria-valuemax={max} aria-valuenow={range ? undefined : values[0]}&gt;
&lt;span className=&quot;kole-m-slider__track&quot;&gt;
&lt;span
className=&quot;kole-m-slider__filled&quot;
style={range ? { left: ratio(values[0]) * 100 + '%', right: 100 - ratio(values[1]) * 100 + '%' } : undefined}
/&gt;
{/* 刻度点:marks=true 时按 step 的可整除位置画(只标位置,不写文案) */}
{marks
? [0, 1, 2, 3, 4].map((i) =&gt; (
&lt;span className=&quot;kole-m-slider__mark&quot; key={'m' + i} style={{ left: i * 25 + '%' }} /&gt;
))
: null}
{range
? values.map((v, i) =&gt; (
&lt;span className=&quot;kole-m-slider__thumb&quot; key={i} style={{ left: ratio(v) * 100 + '%' }} /&gt;
))
: &lt;span className=&quot;kole-m-slider__thumb&quot; style={{ left: ratio(values[0]) * 100 + '%' }} /&gt;}
&lt;/span&gt;
&lt;/span&gt;
{showValue ? (
&lt;span className=&quot;kole-m-slider__value&quot;&gt;{range ? values[0] + '~' + values[1] : values[0]}&lt;/span&gt;
) : null}
{children}
&lt;/div&gt;
);
}
</code></pre>
</div>
</details>
<details class="m-details">
<summary>frameworks-mobile/Slider.vue2.vue · Vue 2 · 170 行</summary>
<div class="m-code">
<div class="m-code-head"><span>frameworks-mobile/Slider.vue2.vue</span><button class="m-copy" type="button" data-copy="code-214">复制</button></div>
<pre id="code-214"><code>&lt;template&gt;
&lt;div
class=&quot;kole-m-slider&quot;
:class=&quot;sliderClass&quot;
@pointerdown=&quot;onPointerDown&quot;
@pointermove=&quot;onPointerMove&quot;
@pointerup=&quot;onPointerUp&quot;
@pointercancel=&quot;onPointerUp&quot;
@keydown=&quot;onKeyDown&quot;
&gt;
&lt;span
ref=&quot;rail&quot;
class=&quot;kole-m-slider__rail&quot;
:role=&quot;range ? 'group' : 'slider'&quot;
:tabindex=&quot;disabled ? null : 0&quot;
:aria-label=&quot;label&quot;
:aria-valuemin=&quot;min&quot;
:aria-valuemax=&quot;max&quot;
:aria-valuenow=&quot;range ? null : values[0]&quot;
&gt;
&lt;span class=&quot;kole-m-slider__track&quot;&gt;
&lt;span
class=&quot;kole-m-slider__filled&quot;
:style=&quot;range ? { left: pct(values[0]), right: 100 - parseFloat(pct(values[1])) + '%' } : null&quot;
&gt;&lt;/span&gt;
&lt;template v-if=&quot;marks&quot;&gt;
&lt;span
v-for=&quot;m in markPoints&quot;
:key=&quot;'m' + m&quot;
class=&quot;kole-m-slider__mark&quot;
:style=&quot;{ left: m + '%' }&quot;
&gt;&lt;/span&gt;
&lt;/template&gt;
&lt;span
v-for=&quot;(v, i) in values&quot;
:key=&quot;'t' + i&quot;
class=&quot;kole-m-slider__thumb&quot;
:style=&quot;{ left: pct(v) }&quot;
&gt;&lt;/span&gt;
&lt;/span&gt;
&lt;/span&gt;
&lt;span v-if=&quot;showValue&quot; class=&quot;kole-m-slider__value&quot;&gt;{{ range ? values[0] + '~' + values[1] : values[0] }}&lt;/span&gt;
&lt;slot&gt;&lt;/slot&gt;
&lt;/div&gt;
&lt;/template&gt;
&lt;script&gt;
/* 滑动选择器(移动端)— 规格 §35
轨道可视高度只有 4px,但**整条热区高度是 44px**:直接点轨道任意位置也要能跳到该处。
浏览器端手势用 Pointer Events(pointerdown / pointermove / pointerup + setPointerCapture);
uni-app 端同一手势改用 @touch*(小程序与 App 端没有 PointerEvent)。
值按 step 吸附并夹在 [min, max];mode=range 时左值不越过右值。
受控:本端不存储值,只回传目标值 change,是否采纳由宿主决定。 */
export default {
name: 'KoleMSlider',
props: {
value: { type: Number, default: 0 },
valueMax: { type: Number, default: 100 },
min: { type: Number, default: 0 },
max: { type: Number, default: 100 },
step: { type: Number, default: 1 },
mode: { type: String, default: 'single' },
marks: { type: Boolean, default: false },
showValue: { type: Boolean, default: true },
disabled: { type: Boolean, default: false },
label: { type: String, default: '滑动选择' }
},
data: function () {
return {
/* null = 跟随 value 属性;拖动后本地记账(与 Collapse 同一约定) */
inner: null,
dragging: false,
active: -1,
markPoints: [0, 25, 50, 75, 100]
};
},
computed: {
range: function () {
return this.mode === 'range';
},
values: function () {
if (this.inner) return this.inner;
return this.range ? [this.value, this.valueMax] : [this.value];
},
sliderClass: function () {
return [
this.range ? 'kole-m-slider--range' : '',
this.marks ? 'kole-m-slider--marks' : '',
this.dragging ? 'is-dragging' : '',
this.disabled ? 'is-disabled' : ''
].filter(Boolean);
}
},
methods: {
pct: function (v) {
if (this.max === this.min) return '0%';
return ((v - this.min) / (this.max - this.min)) * 100 + '%';
},
clamp: function (v) {
var snapped = Math.round((v - this.min) / this.step) * this.step + this.min;
return Math.max(this.min, Math.min(this.max, snapped));
},
valueAt: function (clientX) {
var el = this.$refs.rail;
if (!el) return this.min;
var rect = el.getBoundingClientRect();
if (!rect.width) return this.min;
return this.clamp(this.min + ((clientX - rect.left) / rect.width) * (this.max - this.min));
},
nearest: function (v) {
if (!this.range) return 0;
return Math.abs(this.values[0] - v) &lt;= Math.abs(this.values[1] - v) ? 0 : 1;
},
commit: function (index, v) {
/* 双滑块:左值不越过右值、右值不越过左值(各留一个 step 的间距) */
var next = this.clamp(v);
if (this.range) {
var other = this.values[1 - index];
next = index === 0 ? Math.min(next, other - this.step) : Math.max(next, other + this.step);
}
var out = this.range ? [this.values[0], this.values[1]] : [next];
out[index] = next;
this.inner = out;
this.$emit('change', this.range ? out : out[0]);
},
/* 手势起止都放在根元素上:整个控件(含两侧留白)都是有效起点 */
onPointerDown: function (e) {
if (this.disabled) return;
if (e.target &amp;&amp; e.target.closest &amp;&amp; e.target.closest('.kole-m-slider__value')) return;
var v = this.valueAt(e.clientX);
this.active = this.nearest(v);
this.dragging = true;
this.commit(this.nearest(v), v);
var el = this.$refs.rail;
if (el &amp;&amp; el.setPointerCapture) {
try {
el.setPointerCapture(e.pointerId);
} catch (err) {
/* 指针捕获不可用时靠冒泡事件继续跟随 */
}
}
},
onPointerMove: function (e) {
if (this.active &lt; 0) return;
this.commit(this.active, this.valueAt(e.clientX));
},
onPointerUp: function () {
if (this.active &lt; 0) return;
this.active = -1;
this.dragging = false;
},
onKeyDown: function (e) {
if (this.disabled || this.range) return;
var delta =
e.key === 'ArrowLeft' || e.key === 'ArrowDown'
? -this.step
: e.key === 'ArrowRight' || e.key === 'ArrowUp'
? this.step
: 0;
if (!delta) return;
e.preventDefault();
this.commit(0, this.values[0] + delta);
}
}
};
&lt;/script&gt;
&lt;style src=&quot;./Slider.css&quot;&gt;&lt;/style&gt;
</code></pre>
</div>
</details>
<details class="m-details">
<summary>frameworks-mobile/Slider.vue3.vue · Vue 3 · 156 行</summary>
<div class="m-code">
<div class="m-code-head"><span>frameworks-mobile/Slider.vue3.vue</span><button class="m-copy" type="button" data-copy="code-215">复制</button></div>
<pre id="code-215"><code>&lt;template&gt;
&lt;div
class=&quot;kole-m-slider&quot;
:class=&quot;sliderClass&quot;
@pointerdown=&quot;onPointerDown&quot;
@pointermove=&quot;onPointerMove&quot;
@pointerup=&quot;onPointerUp&quot;
@pointercancel=&quot;onPointerUp&quot;
@keydown=&quot;onKeyDown&quot;
&gt;
&lt;span
ref=&quot;rail&quot;
class=&quot;kole-m-slider__rail&quot;
:role=&quot;range ? 'group' : 'slider'&quot;
:tabindex=&quot;disabled ? null : 0&quot;
:aria-label=&quot;label&quot;
:aria-valuemin=&quot;min&quot;
:aria-valuemax=&quot;max&quot;
:aria-valuenow=&quot;range ? null : values[0]&quot;
&gt;
&lt;span class=&quot;kole-m-slider__track&quot;&gt;
&lt;span
class=&quot;kole-m-slider__filled&quot;
:style=&quot;range ? { left: pct(values[0]), right: 100 - parseFloat(pct(values[1])) + '%' } : null&quot;
&gt;&lt;/span&gt;
&lt;template v-if=&quot;marks&quot;&gt;
&lt;span
v-for=&quot;m in markPoints&quot;
:key=&quot;'m' + m&quot;
class=&quot;kole-m-slider__mark&quot;
:style=&quot;{ left: m + '%' }&quot;
&gt;&lt;/span&gt;
&lt;/template&gt;
&lt;span
v-for=&quot;(v, i) in values&quot;
:key=&quot;'t' + i&quot;
class=&quot;kole-m-slider__thumb&quot;
:style=&quot;{ left: pct(v) }&quot;
&gt;&lt;/span&gt;
&lt;/span&gt;
&lt;/span&gt;
&lt;span v-if=&quot;showValue&quot; class=&quot;kole-m-slider__value&quot;&gt;{{ range ? values[0] + '~' + values[1] : values[0] }}&lt;/span&gt;
&lt;slot&gt;&lt;/slot&gt;
&lt;/div&gt;
&lt;/template&gt;
&lt;script setup&gt;
/* 滑动选择器(移动端)— 规格 §35
轨道可视高度只有 4px,但**整条热区高度是 44px**:直接点轨道任意位置也要能跳到该处。
浏览器端手势用 Pointer Events(pointerdown / pointermove / pointerup + setPointerCapture);
uni-app 端同一手势改用 @touch*(小程序与 App 端没有 PointerEvent)。
值按 step 吸附并夹在 [min, max];mode=range 时左值不越过右值。
受控:本端不存储值,只回传目标值 change,是否采纳由宿主决定。 */
import { computed, ref } from 'vue';
const props = defineProps({
value: { type: Number, default: 0 },
valueMax: { type: Number, default: 100 },
min: { type: Number, default: 0 },
max: { type: Number, default: 100 },
step: { type: Number, default: 1 },
mode: { type: String, default: 'single' },
marks: { type: Boolean, default: false },
showValue: { type: Boolean, default: true },
disabled: { type: Boolean, default: false },
label: { type: String, default: '滑动选择' }
});
const emit = defineEmits(['change']);
const rail = ref(null);
const dragging = ref(false);
const active = ref(-1);
/* null = 跟随 value 属性;拖动后本地记账(与 Collapse 同一约定) */
const inner = ref(null);
const range = computed(() =&gt; props.mode === 'range');
const values = computed(() =&gt; inner.value || (range.value ? [props.value, props.valueMax] : [props.value]));
const markPoints = [0, 25, 50, 75, 100];
const sliderClass = computed(() =&gt; [
range.value ? 'kole-m-slider--range' : '',
props.marks ? 'kole-m-slider--marks' : '',
dragging.value ? 'is-dragging' : '',
props.disabled ? 'is-disabled' : ''
].filter(Boolean));
function pct(v) {
if (props.max === props.min) return '0%';
return ((v - props.min) / (props.max - props.min)) * 100 + '%';
}
function clamp(v) {
const snapped = Math.round((v - props.min) / props.step) * props.step + props.min;
return Math.max(props.min, Math.min(props.max, snapped));
}
function valueAt(clientX) {
const el = rail.value;
if (!el) return props.min;
const rect = el.getBoundingClientRect();
if (!rect.width) return props.min;
return clamp(props.min + ((clientX - rect.left) / rect.width) * (props.max - props.min));
}
function nearest(v) {
if (!range.value) return 0;
return Math.abs(values.value[0] - v) &lt;= Math.abs(values.value[1] - v) ? 0 : 1;
}
function commit(index, v) {
/* 双滑块:左值不越过右值、右值不越过左值(各留一个 step 的间距) */
let next = clamp(v);
if (range.value) {
const other = values.value[1 - index];
next = index === 0 ? Math.min(next, other - props.step) : Math.max(next, other + props.step);
}
const out = range.value ? [values.value[0], values.value[1]] : [next];
out[index] = next;
inner.value = out;
emit('change', range.value ? out : out[0]);
}
/* 手势起止都放在根元素上:整个控件(含两侧留白)都是有效起点 */
function onPointerDown(e) {
if (props.disabled) return;
if (e.target &amp;&amp; e.target.closest &amp;&amp; e.target.closest('.kole-m-slider__value')) return;
const v = valueAt(e.clientX);
active.value = nearest(v);
dragging.value = true;
commit(nearest(v), v);
const el = rail.value;
if (el &amp;&amp; el.setPointerCapture) {
try {
el.setPointerCapture(e.pointerId);
} catch (err) {
/* 指针捕获不可用时靠冒泡事件继续跟随 */
}
}
}
function onPointerMove(e) {
if (active.value &lt; 0) return;
commit(active.value, valueAt(e.clientX));
}
function onPointerUp() {
if (active.value &lt; 0) return;
active.value = -1;
dragging.value = false;
}
function onKeyDown(e) {
if (props.disabled || range.value) return;
const delta =
e.key === 'ArrowLeft' || e.key === 'ArrowDown' ? -props.step : e.key === 'ArrowRight' || e.key === 'ArrowUp' ? props.step : 0;
if (!delta) return;
e.preventDefault();
commit(0, values.value[0] + delta);
}
&lt;/script&gt;
&lt;style src=&quot;./Slider.css&quot;&gt;&lt;/style&gt;
</code></pre>
</div>
</details>
<details class="m-details">
<summary>frameworks-mobile/Slider.uniapp.vue · uni-app(跨端:小程序 / App / H5) · 272 行</summary>
<div class="m-code">
<div class="m-code-head"><span>frameworks-mobile/Slider.uniapp.vue</span><button class="m-copy" type="button" data-copy="code-216">复制</button></div>
<pre id="code-216"><code>&lt;template&gt;
&lt;view
class=&quot;kole-m-slider&quot;
:class=&quot;sliderClass&quot;
@touchstart=&quot;onTouchStart&quot;
@touchmove=&quot;onTouchMove&quot;
@touchend=&quot;onTouchEnd&quot;
@touchcancel=&quot;onTouchEnd&quot;
&gt;
&lt;view class=&quot;kole-m-slider__rail&quot; ref=&quot;rail&quot;&gt;
&lt;view class=&quot;kole-m-slider__track&quot;&gt;
&lt;view class=&quot;kole-m-slider__filled&quot; :style=&quot;filledStyle&quot;&gt;&lt;/view&gt;
&lt;template v-if=&quot;marks&quot;&gt;
&lt;view
v-for=&quot;m in markPoints&quot;
:key=&quot;'m' + m&quot;
class=&quot;kole-m-slider__mark&quot;
:style=&quot;{ left: m + '%' }&quot;
&gt;&lt;/view&gt;
&lt;/template&gt;
&lt;view
v-for=&quot;(v, i) in values&quot;
:key=&quot;'t' + i&quot;
class=&quot;kole-m-slider__thumb&quot;
:style=&quot;{ left: pct(v) }&quot;
:role=&quot;disabled ? '' : 'slider'&quot;
:aria-valuenow=&quot;v&quot;
:aria-valuemin=&quot;min&quot;
:aria-valuemax=&quot;max&quot;
:aria-disabled=&quot;disabled ? 'true' : 'false'&quot;
&gt;&lt;/view&gt;
&lt;/view&gt;
&lt;/view&gt;
&lt;text v-if=&quot;showValue&quot; class=&quot;kole-m-slider__value&quot;&gt;{{ range ? values[0] + '~' + values[1] : values[0] }}&lt;/text&gt;
&lt;slot&gt;&lt;/slot&gt;
&lt;/view&gt;
&lt;/template&gt;
&lt;script setup&gt;
/* uni-app 端 · 滑动选择器(移动端)— 规格 §35
跨端差异(关键的一条):
① **手势只能用 touch 事件**(@touchstart / @touchmove / @touchend)——
小程序与 App 端没有 PointerEvent,也没有 pointerdown/pointermove;
② 坐标从 e.touches[0].clientX 取(uni 的事件对象与 DOM 事件不同,没有统一的 e.clientX 字段,
且 touchend 时 touches 为空数组,需回落到 e.changedTouches[0]);
③ 轨道位置用 ref 上的 uni.createSelectorQuery() 度量(小程序没有 getBoundingClientRect 的同步语义),
度量结果缓存在 layout 里,触摸期间不重复查询;
④ 尺寸用 rpx:88rpx = 375pt 下的 44px 触控最小边长,滑块 40rpx = 20px。 */
import { computed, ref } from 'vue';
const props = defineProps({
value: { type: Number, default: 0 },
valueMax: { type: Number, default: 100 },
min: { type: Number, default: 0 },
max: { type: Number, default: 100 },
step: { type: Number, default: 1 },
mode: { type: String, default: 'single' },
marks: { type: Boolean, default: false },
showValue: { type: Boolean, default: true },
disabled: { type: Boolean, default: false },
label: { type: String, default: '滑动选择' }
});
const emit = defineEmits(['change']);
const rail = ref(null);
const dragging = ref(false);
const active = ref(-1);
/* null = 跟随 value 属性;拖动后本地记账(与 Collapse 同一约定) */
const inner = ref(null);
/* 轨道度量缓存(左边界 + 宽度,单位 px);touchend 后清空,下次按下重新度量 */
const layout = ref(null);
const markPoints = [0, 25, 50, 75, 100];
const range = computed(() =&gt; props.mode === 'range');
const values = computed(() =&gt; inner.value || (range.value ? [props.value, props.valueMax] : [props.value]));
const sliderClass = computed(() =&gt; [
range.value ? 'kole-m-slider--range' : '',
props.marks ? 'kole-m-slider--marks' : '',
dragging.value ? 'is-dragging' : '',
props.disabled ? 'is-disabled' : ''
].filter(Boolean));
const filledStyle = computed(() =&gt; {
if (!range.value) return { right: 100 - ((values.value[0] - props.min) / (props.max - props.min)) * 100 + '%' };
return {
left: ((values.value[0] - props.min) / (props.max - props.min)) * 100 + '%',
right: 100 - ((values.value[1] - props.min) / (props.max - props.min)) * 100 + '%'
};
});
function pct(v) {
if (props.max === props.min) return '0%';
return ((v - props.min) / (props.max - props.min)) * 100 + '%';
}
function clamp(v) {
const snapped = Math.round((v - props.min) / props.step) * props.step + props.min;
return Math.max(props.min, Math.min(props.max, snapped));
}
function nearest(v) {
if (!range.value) return 0;
return Math.abs(values.value[0] - v) &lt;= Math.abs(values.value[1] - v) ? 0 : 1;
}
function commit(index, v) {
let next = clamp(v);
if (range.value) {
const other = values.value[1 - index];
next = index === 0 ? Math.min(next, other - props.step) : Math.max(next, other + props.step);
}
const out = range.value ? [values.value[0], values.value[1]] : [next];
out[index] = next;
inner.value = out;
emit('change', range.value ? out : out[0]);
}
/* 触摸坐标:uni 的事件对象用 touches / changedTouches,没有 DOM 的 clientX */
function clientXOf(e) {
var list = (e &amp;&amp; e.touches &amp;&amp; e.touches.length ? e.touches : null) || (e &amp;&amp; e.changedTouches) || [];
return list.length ? list[0].clientX : null;
}
/* 轨道度量:小程序用 uni.createSelectorQuery 异步拿 left / width */
function measure() {
return new Promise(function (resolve) {
if (typeof uni === 'undefined' || !uni.createSelectorQuery) { resolve(layout.value); return; }
uni
.createSelectorQuery()
.select('.kole-m-slider__rail')
.boundingClientRect(function (rect) {
resolve(rect &amp;&amp; rect.width ? { left: rect.left, width: rect.width } : null);
})
.exec();
});
}
function valueAt(clientX, box) {
if (clientX === null) return null;
if (!box || !box.width) return null;
return clamp(props.min + ((clientX - box.left) / box.width) * (props.max - props.min));
}
function onTouchStart(e) {
if (props.disabled) return;
const x = clientXOf(e);
if (x === null) return;
measure().then(function (box) {
if (!box) return;
layout.value = box;
const v = valueAt(x, box);
if (v === null) return;
active.value = nearest(v);
dragging.value = true;
commit(nearest(v), v);
});
}
function onTouchMove(e) {
if (props.disabled || active.value &lt; 0) return;
const v = valueAt(clientXOf(e), layout.value);
if (v === null) return;
commit(active.value, v);
}
function onTouchEnd() {
if (active.value &lt; 0) return;
active.value = -1;
dragging.value = false;
layout.value = null;
}
&lt;/script&gt;
&lt;style&gt;
.kole-m-slider {
--kole-m-slider-thumb: 40rpx; /* 滑块视觉直径(20px @375pt) */
--kole-m-slider-thumb-active: 48rpx; /* 拖动中的滑块直径(24px) */
--kole-m-slider-track: 8rpx; /* 轨道可视高度(4px) */
--kole-m-touch-target: 88rpx;
--kole-m-font-size-label: 28rpx;
--kole-m-gutter: 32rpx;
box-sizing: border-box;
display: flex;
align-items: center;
width: 100%;
padding-left: var(--kole-m-gutter);
padding-right: var(--kole-m-gutter);
background-color: var(--kole-color-card-bg);
color: var(--kole-color-text-body);
font-size: 32rpx;
}
/* 轨道热区:可视 8rpx 的轨道垂直居中在这条 88rpx 高的带子里 */
.kole-m-slider__rail {
position: relative;
flex: 1;
display: flex;
align-items: center;
min-height: var(--kole-m-touch-target);
}
.kole-m-slider__track {
position: relative;
width: 100%;
height: var(--kole-m-slider-track);
border-radius: 4rpx;
background-color: var(--kole-color-border);
}
.kole-m-slider__filled {
position: absolute;
top: 0;
bottom: 0;
left: 0;
border-radius: 4rpx;
background-color: var(--kole-color-brand);
}
.kole-m-slider__thumb {
position: absolute;
top: 50%;
box-sizing: border-box;
width: var(--kole-m-slider-thumb);
height: var(--kole-m-slider-thumb);
margin-top: -20rpx;
margin-left: -20rpx;
border: 4rpx solid var(--kole-color-brand);
border-radius: 50%;
background-color: var(--kole-color-card-bg);
}
/* 状态 dragging:滑块放大 + 轨道加一层浅底(手指抓住的反馈) */
.kole-m-slider.is-dragging .kole-m-slider__thumb {
width: var(--kole-m-slider-thumb-active);
height: var(--kole-m-slider-thumb-active);
margin-top: -24rpx;
margin-left: -24rpx;
}
.kole-m-slider.is-dragging .kole-m-slider__rail { background-color: var(--kole-color-brand-bg); }
/* 变体 marks=with-marks:轨道上出现刻度点(默认不渲染 —— 刻度是信息不是装饰) */
.kole-m-slider__mark {
display: none;
position: absolute;
top: 50%;
width: 4rpx;
height: 16rpx;
margin-top: -8rpx;
margin-left: -2rpx;
border-radius: 2rpx;
background-color: var(--kole-color-text-placeholder);
}
.kole-m-slider--marks .kole-m-slider__mark { display: block; }
.kole-m-slider__value {
flex-shrink: 0;
min-width: 96rpx;
padding-left: 24rpx;
text-align: right;
color: var(--kole-color-text-secondary);
font-size: var(--kole-m-font-size-label);
}
/* 状态 disabled:整条置灰且不响应 */
.kole-m-slider.is-disabled .kole-m-slider__thumb {
border-color: var(--kole-color-text-disabled);
background-color: var(--kole-color-disabled-bg);
}
.kole-m-slider.is-disabled .kole-m-slider__filled { background-color: var(--kole-color-text-disabled); }
.kole-m-slider.is-disabled .kole-m-slider__value { color: var(--kole-color-text-disabled); }
&lt;/style&gt;
</code></pre>
</div>
</details>
</section>
<section class="m-block">
<h2 id="tests">测试与回归</h2>
<p class="m-sub">断言在真实的 375×640 设备帧里跑(引擎与 PC 侧共用 <code>tests/_runtime.js</code>,触控行为动词来自移动端 <code>tests/mobile/_behaviors.js</code>)。</p>
<p>
<span class="m-status pass">断言 16 条 · 全部通过</span>
<span class="m-status">报告 2026-09-20 16:50:33</span>
</p>
<div class="m-code">
<div class="m-code-head"><span>复现命令</span><button class="m-copy" type="button" data-copy="code-test">复制</button></div>
<pre id="code-test"><code>node site/dev-server.js &amp;
REG_BASE=http://127.0.0.1:3311 node tools/run-mobile-regression.mjs # 全量 5 个组件
npm run verify:mobile-docs # 本页内容完整性 + API 与源码一致性</code></pre>
</div>
</section>
<section class="m-block">
<h2 id="contract">设计契约</h2>
<details class="m-details">
<summary>components/mobile-slider.json(点击展开原始 JSON)</summary>
<div class="m-code"><pre><code>{
"schemaVersion": 1,
"sourceKind": "authored-spec",
"provenance": "authored-in-repo",
"specFile": "spec/移动端规格.md",
"specSection": "35 · 滑动选择器 Slider",
"confidence": "high",
"slug": "mobile-slider",
"name": "滑动选择器 Slider",
"semanticTypeCandidates": [
"slider",
"range-slider",
"gesture-input"
],
"variantDimensions": [
{
"name": "mode",
"values": [
"single",
"range"
]
},
{
"name": "marks",
"values": [
"with-marks",
"none"
]
},
{
"name": "showValue",
"values": [
"true",
"false"
]
}
],
"representativeVariants": [
{
"mode": "single",
"marks": "none",
"showValue": "true",
"label": "默认(单滑块 + 右侧数值)"
},
{
"mode": "single",
"marks": "with-marks",
"showValue": "true",
"label": "带刻度(轨道上标出等分点)"
},
{
"mode": "range",
"marks": "none",
"showValue": "true",
"label": "双滑块区间(左值不越过右值)"
}
],
"anatomy": {
"slider": "根元素,一行里放进「轨道热区 + 数值」",
"rail": "轨道热区,44px 高的可聚焦区域,承载指针事件与锚点",
"track": "轨道本体(可视 4px),承载选中段与刻度的边界",
"filled": "选中段,从起点铺到滑块位置",
"thumb": "滑块,位置由当前值的百分比决定;拖动中放大作为「抓住了」的反馈",
"mark": "刻度点,仅 marks=with-marks 时出现(只标位置,不写文案)"
},
"structurePatterns": {
"mode": "single(单滑块)/ range(双滑块,左值不越过右值)",
"marks": "with-marks(轨道上标出等分点)/ none(不标刻度)",
"showValue": "true(右侧显示当前值)/ false(不显示数值)"
},
"usageHints": [
"在一个连续区间里用一次滑动选出数值(预算上限、价格区间、音量、亮度)",
"轨道可视高度只有 4px,但整条轨道所在的行必须是 44px 高的热区",
"按下任意位置就要跳到该处 —— 触屏上要求用户精确抓住 20px 的滑块是点不中的",
"取值按 step 吸附并夹在 [min, max];mode=range 时左值不越过右值、右值不越过左值",
"值变化立即触发 change 事件(连续回传),不做防抖;松手时才做业务提交"
],
"doNotInvent": [
"数值输入框与滑块的双向绑定(宿主自行组合 Input,本组件只回传值)",
"刻度上的文案标注与区间高亮(只标位置,不写文案)",
"拖动结束后的「撤销 / 重置」流程",
"纵向滑块与圆形表盘式选择器"
],
"unknowns": [
"双滑块的最小间距是否应当是 1 个 step(当前是),还是用户可配",
"刻度数量是否应由 step 推导(当前按 5 等分固定标点)",
"拖动中是否要把当前值放大显示在滑块上方(气泡提示)"
],
"interaction": [
"轨道热区高 44px,**按下任意位置即跳到该处**,随后横向拖动连续改值;不要求抓住滑块",
"手势是「按下 + 拖动」一次连续动作,不响应长按或双击;横向分量优先,纵向滚动不被吞掉(touch-action: pan-y)",
"取值按 step 吸附并夹在 [min, max];mode=range 时左值不越过右值、右值不越过左值",
"拖动期间滑块放大到 24px,松手后 150ms 回弹;减少动态偏好下瞬时切换",
"值变化立即触发 change 事件(连续回传),不做防抖;松手时才做业务提交"
],
"accessibility": [
"单滑块:轨道热区是 role=\"slider\" 且可聚焦,写 aria-valuemin / aria-valuemax / aria-valuenow / aria-label",
"双滑块:外层是 role=\"group\" + aria-label,每个滑块各自持有 aria-valuenow(两条独立数值)",
"禁用时写 aria-disabled=\"true\",读屏会播报不可用",
"键盘:聚焦后左右方向键按 step 步进(触屏之外的替代路径)"
],
"api": {
"source": "implementation",
"note": "props / events / slots 为 6 端实现的公共接口(说明文字取自规格对应小节)。字段名与各端源码逐名核对:node tools/verify-mobile-docs.mjs",
"requiredNote": "「必传」按严格定义:实现里**没有默认值**时才为 Y(本门禁逐条核对 props 与各端源码的默认值,防止契约与实现脱节)。",
"props": [
{
"name": "value",
"type": "number",
"default": "0",
"desc": "受控当前值(range 模式下是左值),与 aria-valuenow 同步写(规格 §35.5)",
"required": false
},
{
"name": "valueMax",
"type": "number",
"default": "100",
"desc": "mode=range 时的右值(single 模式忽略)(规格 §35.3)",
"required": false
},
{
"name": "min",
"type": "number",
"default": "0",
"desc": "区间下限,拖动与键盘步进都不会低于它(规格 §35.5)",
"required": false
},
{
"name": "max",
"type": "number",
"default": "100",
"desc": "区间上限,拖动与键盘步进都不会高于它(规格 §35.5)",
"required": false
},
{
"name": "step",
"type": "number",
"default": "1",
"desc": "取值吸附步长;也是双滑块的最小间距与键盘步进量(规格 §35.5)",
"required": false
},
{
"name": "mode",
"type": "'single' | 'range'",
"default": "'single'",
"desc": "变体 mode:单滑块还是双滑块区间(规格 §35.3)",
"required": false
},
{
"name": "marks",
"type": "boolean",
"default": "false",
"desc": "变体 marks:轨道上是否标出等分刻度点(规格 §35.3)",
"required": false
},
{
"name": "showValue",
"type": "boolean",
"default": "true",
"desc": "变体 showValue:是否在轨道右侧显示当前值(规格 §35.3)",
"required": false
},
{
"name": "disabled",
"type": "boolean",
"default": "false",
"desc": "状态 disabled:整条置灰且不响应,指针与键盘都不改值(规格 §35.4)",
"required": false
},
{
"name": "label",
"type": "string",
"default": "'滑动选择'",
"desc": "无障碍名称,落到 role=\"slider\" 的 aria-label(规格 §35.6)",
"required": false
}
],
"events": [
{
"name": "change",
"params": "(value)",
"desc": "拖动或键盘步进改值时触发,连续回传当前值(range 模式回传 [左值, 右值])(规格 §35.5)"
}
],
"slots": [
{
"name": "default",
"desc": "数值之后的自定义内容(如单位文案,规格 §35.2 slider)"
}
]
},
"variantClasses": {
"mode": {
"single": [],
"range": [
".kole-m-slider--range"
]
},
"marks": {
"none": [],
"with-marks": [
".kole-m-slider--marks",
".kole-m-slider__mark"
]
},
"showValue": {
"true": [],
"false": []
}
},
"demos": [
{
"id": "basic",
"group": "01 组件类型",
"title": "基础用法",
"desc": "在轨道上按住拖动即改值;轨道可视 4px、热区 44px 高。",
"variant": "mode=single"
},
{
"id": "steps",
"group": "01 组件类型",
"title": "带刻度",
"desc": "marks=with-marks:轨道上标出等分点;取值仍按 step 吸附。",
"variant": "marks=with-marks"
},
{
"id": "range",
"group": "01 组件类型",
"title": "双滑块区间",
"desc": "mode=range:两个滑块各自独立拖动,左值不会越过右值。",
"variant": "mode=range"
},
{
"id": "dragging",
"group": "02 组件状态",
"title": "拖动中",
"desc": "滑块由 20px 放大到 24px,并给轨道加一层浅底作「抓住了」的反馈。",
"variant": "状态 dragging"
},
{
"id": "disabled",
"group": "02 组件状态",
"title": "禁用",
"desc": "整条置灰且不响应:读屏播报不可用,指针事件不再改值。",
"variant": "disabled=true"
}
],
"related": [
{
"slug": "mobile-stepper",
"why": "有界区间里加减小整数、且需要精确值时用 Stepper;数值连续、更看重「大致位置」时才用 Slider"
},
{
"slug": "mobile-input",
"why": "需要键盘输入精确数值(金额、编号)时用 Input + 数字键盘;Slider 只适合粗选"
},
{
"slug": "mobile-form",
"why": "滑块作为表单里的一个字段时,标签、必填星号与错误提示由 Form 的 item 提供"
}
]
}</code></pre></div>
</details>
</section>
<footer class="m-foot">
<p>生成物:本页由 <code>node tools/build-mobile.mjs</code> 从契约 <code>components/mobile-slider.json</code> 与 6 端源码生成,请勿手改。</p>
<p>规格原文:<code>.design_library/kole-ui-mobile/spec/移动端规格.md</code> · 隔离门禁:<code>node tools/verify-mobile-isolation.mjs</code></p>
</footer>
</main>
</div>
<script>
(function () {
/* 左栏:≤1000px 默认收起(手机上不让导航占满一屏),跨断点时同步一次;不监听 resize,
免得跟用户手动的展开/收起抢状态。桌面不设 open 之外的动作。 */
var side = document.querySelector('details.m-side');
if (side && window.matchMedia) {
var mq = window.matchMedia('(max-width: 1000px)');
var sync = function () { if (mq.matches) side.removeAttribute('open'); else side.setAttribute('open', ''); };
sync();
if (mq.addEventListener) mq.addEventListener('change', sync);
else if (mq.addListener) mq.addListener(sync);
}
/* 单演示预览帧按内容高度自适应(同源可读 contentDocument;上限 360px、下限 140px)。
固定高度会在内容少的演示上留一大片空白(实测 SwipeCell 单个演示只用 ~120px)。 */
function fitDemoFrames() {
var frames = document.querySelectorAll('.m-demo-stage iframe');
Array.prototype.forEach.call(frames, function (f) {
var fit = function () {
try {
var d = f.contentDocument;
if (!d || !d.body) return;
/* 只量 body.scrollHeight:documentElement.scrollHeight 等于**帧视口高度**(html 撑满视口),
拿它做 max 会形成「帧高→视口高→量到的高度」反馈环,永远收敛在初始值(实测锁在 308px)。 */
var h = d.body.scrollHeight;
if (!h) h = d.documentElement ? d.documentElement.scrollHeight : 0;
if (!h) return;
f.style.height = Math.min(360, Math.max(140, h + 8)) + 'px';
} catch (e) { /* 跨源或未就绪:保持 CSS 兜底高度 */ }
};
f.addEventListener('load', function () { fit(); setTimeout(fit, 300); });
fit();
});
}
fitDemoFrames();
window.addEventListener('load', fitDemoFrames);
/* 主题模式(三态):与 PC 完全同一约定 —— localStorage[kole-mode] ∈ light|dark|auto,
auto 跟随系统,反色靠 html.kole-dark(令牌文件里已写好该组,两端共用一份令牌)。
用户在 PC 站选夜间,进移动端站也是夜间。 */
var MODE_KEY = 'kole-mode';
var MODE_LABEL = { light: '日间模式', dark: '夜间模式', auto: '自动模式' };
function readMode() {
try { var v = localStorage.getItem(MODE_KEY); if (v === 'light' || v === 'dark' || v === 'auto') return v; } catch (e) {}
return 'auto';
}
function systemDark() {
return !!(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches);
}
function applyMode(mode) {
var dark = mode === 'dark' || (mode === 'auto' && systemDark());
document.documentElement.classList.toggle('kole-dark', dark);
document.documentElement.style.colorScheme = dark ? 'dark' : 'light';
var cur = document.getElementById('m-mode-cur');
if (cur) cur.textContent = MODE_LABEL[mode] || MODE_LABEL.auto;
Array.prototype.forEach.call(document.querySelectorAll('.m-menu-opt'), function (o) {
o.setAttribute('aria-selected', o.getAttribute('data-mode') === mode ? 'true' : 'false');
});
/* 三态图标与 PC 同口径:显式选择时按时态给图标(日间=月亮 / 夜间=太阳),
自动模式下恒为「跟随系统」的显示器图标(此时两个显式图标都不显示)。 */
var moon = document.getElementById('m-icon-moon');
var sun = document.getElementById('m-icon-sun');
var auto = document.getElementById('m-icon-auto');
if (moon) moon.style.display = (mode === 'light') ? '' : 'none';
if (sun) sun.style.display = (mode === 'dark') ? '' : 'none';
if (auto) auto.style.display = (mode === 'auto') ? '' : 'none';
}
function bindModeMenu() {
var trigger = document.getElementById('m-mode-trigger');
var menu = document.getElementById('m-mode-menu');
if (!trigger || !menu) return;
var close = function (refocus) {
menu.classList.add('hidden'); trigger.setAttribute('aria-expanded', 'false');
if (refocus) trigger.focus();
};
trigger.addEventListener('click', function () {
var open = menu.classList.toggle('hidden');
trigger.setAttribute('aria-expanded', open ? 'false' : 'true');
});
trigger.addEventListener('keydown', function (e) {
if (e.key === 'Escape') close(true);
});
Array.prototype.forEach.call(menu.querySelectorAll('.m-menu-opt'), function (opt) {
opt.addEventListener('click', function () {
var mode = opt.getAttribute('data-mode');
try { localStorage.setItem(MODE_KEY, mode); } catch (e) {}
applyMode(mode);
close(true);
});
});
document.addEventListener('click', function (e) {
if (!menu.classList.contains('hidden') && !menu.contains(e.target) && !trigger.contains(e.target)) close(false);
});
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && !menu.classList.contains('hidden')) close(true);
});
/* 自动模式下跟随系统切换 */
if (window.matchMedia) {
var mq = window.matchMedia('(prefers-color-scheme: dark)');
var onChange = function () { if (readMode() === 'auto') applyMode('auto'); };
if (mq.addEventListener) mq.addEventListener('change', onChange);
else if (mq.addListener) mq.addListener(onChange);
}
applyMode(readMode());
}
bindModeMenu();
/* 版本下拉:与 PC 顶栏同一实现口径 —— 清单从部署根取(<prefix>/versions.json 与
<prefix>/site/versions.json 两份按序尝试,取有可用条目的那份),拿不到就退回不可点的
纯角标(data-single="1"),不留一个点不开的下拉。
清单不许把用户带去外站:path 只允许 ".."(站点根自身)或 x.y.z 快照目录名。 */
var VERSION_KEY_PATH = '..';
function isValidVersionPath(p) {
if (typeof p !== 'string' || !p.length || p.indexOf('\\') >= 0 || p.indexOf('//') >= 0) return false;
if (p === VERSION_KEY_PATH) return true;
return /^\d+\.\d+\.\d+$/.test(p);
}
/* 清单里的 path 相对部署根,".." = /site/、x.y.z = /x.y.z/site/。
当前页可能不在部署根下(快照页),故先还原部署根绝对路径再算回相对当前页。 */
function versionHref(absSite) {
var i = location.pathname.indexOf('/site/');
var prefix = i >= 0 ? location.pathname.slice(0, i) : '';
if (!prefix || absSite.indexOf(prefix + '/') !== 0) return absSite;
var rest = absSite.slice(prefix.length + 1);
var up = '';
var segs = prefix.slice(1).split('/');
for (var k = 0; k < segs.length; k++) up += "../";
return up + rest;
}
function buildVersionList(doc) {
var raw = doc && doc.versions;
if (!raw || !raw.length) return [];
var i = location.pathname.indexOf('/site/');
var prefix = i >= 0 ? location.pathname.slice(0, i) : '';
var list = [];
for (var k = 0; k < raw.length; k++) {
var v = raw[k] || {};
var ver = v.version ? String(v.version) : '';
var path = v.path ? String(v.path) : '';
if (!ver || !isValidVersionPath(path)) continue;
var absSite = (path === VERSION_KEY_PATH ? '' : '/' + path) + '/site/';
list.push({ version: ver, date: v.date ? String(v.date) : '', latest: !!v.latest, href: versionHref(absSite), absSite: absSite, current: false });
}
if (list.length) {
/* 当前版本 = 绝对路径上命中的最长前缀(最具体的那个)——与 PC 的判定一致,
快照页与根站点都能算对。 */
var here = location.pathname;
var best = -1;
for (var j = 0; j < list.length; j++) {
if (here.indexOf(list[j].absSite) === 0 && list[j].absSite.length > best) best = list[j].absSite.length;
}
for (var m = 0; m < list.length; m++) list[m].current = best >= 0 && list[m].absSite.length === best;
if (best < 0 && list[0]) list[0].current = true;
}
return list;
}
function bindVersionMenu() {
var trigger = document.getElementById('m-ver-trigger');
var menu = document.getElementById('m-ver-menu');
var chip = document.getElementById('m-ver');
if (!trigger || !menu) return;
var close = function (refocus) {
menu.classList.add('hidden'); trigger.setAttribute('aria-expanded', 'false');
if (refocus) trigger.focus();
};
var interactive = false;
var render = function (list) {
interactive = !!(list && list.length);
/* 只有清单拿不到才退回纯角标;只要有一版就是下拉(里面至少能看清当前是哪个版本) */
trigger.setAttribute('data-single', interactive ? '0' : '1');
if (!interactive) { menu.classList.add("hidden"); return; }
var cur = null;
for (var k = 0; k < list.length; k++) if (list[k].current) cur = list[k];
if (chip) chip.textContent = 'v' + ((cur && cur.version) || VERSION);
/* 重建选项(幂等:重复调用不会叠加) */
Array.prototype.slice.call(menu.querySelectorAll('.m-ver-opt')).forEach(function (n) { n.remove(); });
list.forEach(function (it) {
var opt = document.createElement('button');
opt.type = 'button'; opt.className = 'm-ver-opt'; opt.setAttribute('role', 'option');
opt.setAttribute('data-version', it.version);
opt.setAttribute('aria-selected', it.current ? 'true' : 'false');
var tag = it.current ? '当前浏览' : (it.latest ? '最新版' : (it.date || ''));
opt.innerHTML = '<span class="m-ver-opt-num">v' + it.version + '</span><span class="m-ver-opt-tag">' + tag + '</span>';
if (it.href) opt.setAttribute('data-href', it.href);
opt.addEventListener("click", function () {
if (!it.current && it.href) location.href = it.href;
else close(true);
});
menu.appendChild(opt);
});
};
trigger.addEventListener('click', function () {
if (!interactive) return;
if (menu.classList.contains('hidden')) { menu.classList.remove('hidden'); trigger.setAttribute('aria-expanded', 'true'); }
else close(false);
});
trigger.addEventListener('keydown', function (e) {
if (!interactive) return;
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { e.preventDefault(); menu.classList.remove('hidden'); trigger.setAttribute('aria-expanded', 'true'); }
else if (e.key === 'Escape') close(true);
});
document.addEventListener('click', function (e) {
if (!menu.classList.contains('hidden') && !menu.contains(e.target) && !trigger.contains(e.target)) close(false);
});
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && !menu.classList.contains('hidden')) close(true);
});
/* 两份清单按序尝试(不并行:快照页那份必然 404,并行会把 404 记进控制台,
而站点门禁的「零控制台错误」是硬断言)。 */
if (!window.fetch || location.protocol === 'file:') return;
var i = location.pathname.indexOf('/site/');
if (i < 0) return;
var prefix = location.pathname.slice(0, i);
var urls = [prefix + '/versions.json', prefix + '/site/versions.json'];
var attempt = function (idx) {
if (idx >= urls.length) return;
fetch(urls[idx], { cache: 'no-store' })
.then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
.then(function (doc) { var list = buildVersionList(doc); if (list.length) render(list); else attempt(idx + 1); })
.catch(function () { attempt(idx + 1); });
};
attempt(0);
}
bindVersionMenu();
function done(btn) {
var old = btn.textContent; btn.textContent = '已复制'; btn.classList.add('is-done');
setTimeout(function () { btn.textContent = old; btn.classList.remove('is-done'); }, 1200);
}
document.addEventListener('click', function (e) {
var btn = e.target && e.target.closest ? e.target.closest('.m-copy') : null;
if (!btn) return;
var src = document.getElementById(btn.getAttribute('data-copy') || '');
var text = src ? (src.innerText || src.textContent || '') : '';
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(function () { done(btn); }, function () { fallback(btn, text); });
} else { fallback(btn, text); }
});
function fallback(btn, text) {
try {
var ta = document.createElement('textarea'); ta.value = text; ta.setAttribute('readonly', '');
ta.style.position = 'fixed'; ta.style.opacity = '0'; document.body.appendChild(ta);
ta.select(); document.execCommand("copy"); document.body.removeChild(ta); done(btn);
} catch (err) { btn.textContent = '复制失败'; }
}
})();
</script>
</body>
</html>