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) => (max === min ? 0 : (v - min) / (max - min)); const clamp = (v) => { 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) <= 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 && e.target.closest && 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 && el.setPointerCapture) { try { el.setPointerCapture(e.pointerId); } catch (err) { /* 指针捕获不可用时靠冒泡事件继续跟随 */ } } } function onPointerMove(e) { if (active < 0) return; commit(active, valueAt(e.clientX)); } function onPointerUp() { if (active < 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 (