feat(slider): 添加仅可拖拽滑块的音量滑条组件

- 新增 ThumbOnlyRangeSlider 组件,限制只允许拖拽滑块调整数值
- 替换 Home 页面中音量滑条为 ThumbOnlyRangeSlider 组件
- 移除原有的滑条拖拽检测和事件处理逻辑
- 统一滑条拖拽开始/结束以及释放时的回调处理
- 修正 BrandDrawer 组件中响应式高度的样式支持问题
This commit is contained in:
eafonyang
2026-09-15 17:09:22 +08:00
parent 19172275a9
commit b008307f82
3 changed files with 175 additions and 65 deletions
+12 -64
View File
@@ -12,6 +12,7 @@
import { useDevice } from "@/contexts/DeviceContext";
import ConnectionPlaceholder from "@/components/ConnectionPlaceholder";
import BottomNav from "@/components/BottomNav";
import ThumbOnlyRangeSlider from "@/components/ThumbOnlyRangeSlider";
import {
AlertDialog,
AlertDialogAction,
@@ -254,8 +255,6 @@ export default function Home() {
const [localVol, setLocalVol] = useState(deviceState?.volume ?? 100);
const isDragging = useRef(false);
const volumeSliderRef = useRef<HTMLInputElement>(null);
const volumeSliderDragAllowedRef = useRef(false);
const knobDraggingRef = useRef(false);
const knobActivePointerIdRef = useRef<number | null>(null);
const knobStartAngleRef = useRef(0);
@@ -272,16 +271,6 @@ export default function Home() {
knobLastVolRef.current = localVol;
}, [localVol]);
const isPointerOnVolumeSliderThumb = useCallback((clientX: number) => {
const el = volumeSliderRef.current;
if (!el) return false;
const rect = el.getBoundingClientRect();
if (rect.width <= 0) return false;
const thumbCenterX = rect.left + (localVol / 200) * rect.width;
const hitRadius = Math.max(28, rect.width * 0.05);
return Math.abs(clientX - thumbCenterX) <= hitRadius;
}, [localVol]);
const handlePowerOff = useCallback(async () => {
if (powerActionRef.current) return;
powerActionRef.current = true;
@@ -700,67 +689,26 @@ export default function Home() {
background: "#00FFF6",
boxShadow: "0 0 8px rgba(0,255,246,0.5)"
}} />
<input
ref={volumeSliderRef}
type="range" min={0} max={200} value={localVol}
<ThumbOnlyRangeSlider
min={0}
max={200}
value={localVol}
disabled={volumePassthroughActive}
className="cyan-slider cyan-slider-thumb-only relative z-10 disabled:opacity-50"
onChange={(e) => {
className={volumePassthroughActive ? "opacity-50" : undefined}
onChange={(v) => {
if (volumePassthroughActive) return;
if (!volumeSliderDragAllowedRef.current) {
e.target.value = String(localVol);
return;
}
const v = Number(e.target.value);
setLocalVol(v);
}}
onPointerDown={(e) => {
onDragStart={() => {
if (volumePassthroughActive) return;
const onThumb = isPointerOnVolumeSliderThumb(e.clientX);
volumeSliderDragAllowedRef.current = onThumb;
if (!onThumb) {
e.preventDefault();
return;
}
isDragging.current = true;
}}
onPointerUp={(e) => {
const allowed = volumeSliderDragAllowedRef.current;
volumeSliderDragAllowedRef.current = false;
onDragEnd={() => {
isDragging.current = false;
if (!allowed || volumePassthroughActive) return;
applyVolume(Number(e.currentTarget.value));
}}
onPointerCancel={(e) => {
const allowed = volumeSliderDragAllowedRef.current;
volumeSliderDragAllowedRef.current = false;
isDragging.current = false;
if (!volumePassthroughActive && allowed) {
applyVolume(Number(e.currentTarget.value));
}
}}
onBlur={(e) => {
const allowed = volumeSliderDragAllowedRef.current;
volumeSliderDragAllowedRef.current = false;
isDragging.current = false;
if (!volumePassthroughActive && allowed) {
applyVolume(Number(e.currentTarget.value));
}
}}
onKeyUp={(e) => {
const k = e.key;
if (
k === "ArrowLeft" ||
k === "ArrowRight" ||
k === "ArrowUp" ||
k === "ArrowDown" ||
k === "Home" ||
k === "End" ||
k === "PageUp" ||
k === "PageDown"
) {
applyVolume(Number(e.currentTarget.value));
}
onRelease={(v) => {
if (volumePassthroughActive) return;
applyVolume(v);
}}
/>
</div>
@@ -164,7 +164,7 @@ export function BrandDrawer({
onClick={onClose}
/>
<div
className="relative z-10 flex h-[58dvh] flex-shrink-0 flex-col overflow-hidden rounded-t-[18px] bg-white shadow-[0_-8px_32px_rgba(0,0,0,0.35)]"
className="relative z-10 flex h-[58vh] max-h-full flex-shrink-0 flex-col overflow-hidden rounded-t-[18px] bg-white shadow-[0_-8px_32px_rgba(0,0,0,0.35)] supports-[height:1dvh]:h-[58dvh]"
onClick={(e) => e.stopPropagation()}
>
<div className="flex shrink-0 items-center justify-center pt-2 pb-1">