feat: 移动端UI精致化优化
- 添加页面过渡动画(淡入+滑动) - 优化统计卡片视觉,添加渐变色和微动效 - 实现列表项左滑删除手势(编辑+删除按钮) - 优化底部导航,缩小高度并添加点击波纹效果 - 改进空状态,添加插画和引导按钮 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
import { useState, useRef, useCallback } from 'react';
|
||||
|
||||
export default function SwipeableItem({
|
||||
children,
|
||||
onSwipeLeft,
|
||||
renderRightActions,
|
||||
swipeThreshold = 80,
|
||||
}) {
|
||||
const [isSwiping, setIsSwiping] = useState(false);
|
||||
const [translateX, setTranslateX] = useState(0);
|
||||
const [isRevealed, setIsRevealed] = useState(false);
|
||||
const startXRef = useRef(0);
|
||||
const currentXRef = useRef(0);
|
||||
const itemRef = useRef(null);
|
||||
|
||||
const handleTouchStart = useCallback((e) => {
|
||||
startXRef.current = e.touches[0].clientX;
|
||||
currentXRef.current = startXRef.current;
|
||||
setIsSwiping(true);
|
||||
}, []);
|
||||
|
||||
const handleTouchMove = useCallback((e) => {
|
||||
if (!isSwiping) return;
|
||||
|
||||
currentXRef.current = e.touches[0].clientX;
|
||||
const diff = currentXRef.current - startXRef.current;
|
||||
|
||||
// Only allow swiping left (negative diff)
|
||||
if (diff < 0) {
|
||||
// Limit the swipe distance
|
||||
const newTranslateX = Math.max(diff, -swipeThreshold - 20);
|
||||
setTranslateX(newTranslateX);
|
||||
} else if (isRevealed) {
|
||||
// If already revealed, allow swiping right to close
|
||||
const newTranslateX = Math.min(0, -swipeThreshold + diff);
|
||||
setTranslateX(newTranslateX);
|
||||
}
|
||||
}, [isSwiping, isRevealed, swipeThreshold]);
|
||||
|
||||
const handleTouchEnd = useCallback(() => {
|
||||
setIsSwiping(false);
|
||||
const diff = currentXRef.current - startXRef.current;
|
||||
|
||||
if (diff < -swipeThreshold / 2 || (isRevealed && diff > -swipeThreshold / 2)) {
|
||||
// Reveal actions
|
||||
setTranslateX(-swipeThreshold);
|
||||
setIsRevealed(true);
|
||||
} else {
|
||||
// Snap back
|
||||
setTranslateX(0);
|
||||
setIsRevealed(false);
|
||||
}
|
||||
}, [isRevealed, swipeThreshold]);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setTranslateX(0);
|
||||
setIsRevealed(false);
|
||||
}, []);
|
||||
|
||||
const handleActionClick = useCallback((callback) => {
|
||||
callback?.();
|
||||
handleClose();
|
||||
}, [handleClose]);
|
||||
|
||||
return (
|
||||
<div className="swipeable-item" ref={itemRef}>
|
||||
<div
|
||||
className={`swipeable-content ${isSwiping ? 'is-swiping' : ''}`}
|
||||
style={{
|
||||
transform: `translateX(${translateX}px)`,
|
||||
transition: isSwiping ? 'none' : 'transform 0.2s ease-out',
|
||||
}}
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchMove={handleTouchMove}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
onTouchCancel={handleTouchEnd}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
{renderRightActions && (
|
||||
<div
|
||||
className="swipeable-actions"
|
||||
style={{
|
||||
opacity: isRevealed || translateX < 0 ? 1 : 0,
|
||||
transition: 'opacity 0.2s ease-out',
|
||||
}}
|
||||
>
|
||||
{renderRightActions(handleActionClick)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user