Skip to content
Merged

Dev #32

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
6d4974f
Merge pull request #7 from ubdmf/master
capykyo Feb 24, 2025
a9f46e8
Merge pull request #8 from capykyo/dev
capykyo Feb 24, 2025
4fabd9a
Merge pull request #9 from ubdmf/master
capykyo Feb 24, 2025
3b24d7c
Merge pull request #14 from ubdmf/master
capykyo Feb 27, 2025
6bb8f74
create: test env
ubdmf Mar 2, 2025
4543865
Merge pull request #21 from ubdmf/master
capykyo Mar 5, 2025
c42d099
refactor: enhance localStorage handling and improve error management …
ubdmf Nov 6, 2025
aca1413
feat: add bookshelf management features including BookForm, BookList,…
ubdmf Nov 8, 2025
8b899f0
test: add comprehensive unit tests for BookshelfPage component, cover…
ubdmf Nov 8, 2025
e77ac6e
feat: integrate tooltip functionality across multiple components incl…
ubdmf Nov 8, 2025
5527a21
chore: merge dev into master
ubdmf Nov 8, 2025
2cdbe87
refactor: enhance localStorage handling and improve error management …
ubdmf Nov 6, 2025
8d65d17
feat: add bookshelf management features including BookForm, BookList,…
ubdmf Nov 8, 2025
b02e131
test: add comprehensive unit tests for BookshelfPage component, cover…
ubdmf Nov 8, 2025
67d6fb0
feat: integrate tooltip functionality across multiple components incl…
ubdmf Nov 8, 2025
e7ff921
feat: restrict first-visit-test to dev environment and add storage ma…
ubdmf Nov 9, 2025
e3279af
feat: 实现多网站书籍解析系统
ubdmf Nov 9, 2025
84ee32b
fix: 优化请求头和错误处理,修复403错误
ubdmf Nov 9, 2025
56b36d3
fix: update Axios mock type to include optional status field for impr…
ubdmf Nov 9, 2025
6d1afad
fix: update pagination logic to handle URL query changes
ubdmf Nov 9, 2025
0d2558c
feat: enhance API Key management and client instantiation across comp…
ubdmf Nov 9, 2025
1d0de58
fix: improve error handling in AiReadingPage and aiReader API
ubdmf Nov 9, 2025
b36ca8f
Merge dev into master
ubdmf Nov 9, 2025
14a6253
feat: add Git workflow best practices document to minimize merge conf…
ubdmf Nov 9, 2025
619ac74
create: test env
ubdmf Mar 2, 2025
78b0215
feat: add bookshelf management features including BookForm, BookList,…
ubdmf Nov 8, 2025
fce0abf
test: add comprehensive unit tests for BookshelfPage component, cover…
ubdmf Nov 8, 2025
5dfeb12
feat: add Git workflow best practices document to minimize merge conf…
ubdmf Nov 9, 2025
7d3dcba
refactor: improve layout and styling of ReadingDuration component
ubdmf Nov 10, 2025
2cdd3b4
feat: enhance SwipeContainer with improved swipe functionality and vi…
ubdmf Nov 10, 2025
6550479
Merge branch 'dev'
ubdmf Nov 10, 2025
ebc6859
feat: add Slider and Switch components using Radix UI
ubdmf Nov 13, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
724 changes: 724 additions & 0 deletions GIT_WORKFLOW.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@
"@radix-ui/react-progress": "^1.1.8",
"@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slider": "^1.3.6",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-toast": "^1.2.6",
"@radix-ui/react-tooltip": "^1.2.8",
"@tailwindcss/typography": "^0.5.16",
Expand Down
71 changes: 71 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 8 additions & 2 deletions src/components/ReadingDuration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ const ReadingTimer: React.FC = () => {

useEffect(() => {
// 从 localStorage 恢复计时
const savedTime = typeof window !== "undefined" && localStorage.getItem("readingTime");
const savedTime =
typeof window !== "undefined" && localStorage.getItem("readingTime");
if (savedTime) {
setTimeSpent(parseInt(savedTime, 10));
}
Expand Down Expand Up @@ -57,7 +58,12 @@ const ReadingTimer: React.FC = () => {

return (
<div className="text-sm text-gray-500 dark:text-gray-400">
<p>阅读时间: {formatDate(timeSpent)}</p>
<p className="whitespace-nowrap">
阅读时间:{" "}
<span className="font-mono tabular-nums min-w-[80px] inline-block">
{formatDate(timeSpent)}
</span>
</p>
</div>
);
};
Expand Down
168 changes: 160 additions & 8 deletions src/components/article/SwipeContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { useRef, useEffect, ReactNode } from "react";
import { useRef, useEffect, ReactNode, useState } from "react";

interface SwipeContainerProps {
children: ReactNode;
onSwipeLeft?: () => void;
onSwipeRight?: () => void;
threshold?: number;
className?: string;
style?: React.CSSProperties;
}

export const SwipeContainer = ({
Expand All @@ -14,35 +15,144 @@ export const SwipeContainer = ({
onSwipeRight,
threshold = 120,
className = "",
style,
}: SwipeContainerProps) => {
const touchStartX = useRef(0);
const touchStartY = useRef(0);
const touchEndX = useRef(0);
const touchEndY = useRef(0);
const containerRef = useRef<HTMLDivElement>(null);
const contentRef = useRef<HTMLDivElement>(null);
const [translateX, setTranslateX] = useState(0);
const [isSwiping, setIsSwiping] = useState(false);
const [swipeDirection, setSwipeDirection] = useState<"left" | "right" | null>(
null
);
const [isTransitioning, setIsTransitioning] = useState(false);
const contentKeyRef = useRef(0);

useEffect(() => {
let isActive = false;

const handleTouchStart = (e: TouchEvent) => {
touchStartX.current = e.touches[0].clientX;
touchStartY.current = e.touches[0].clientY;
isActive = true;
setIsSwiping(true);
setTranslateX(0);
setSwipeDirection(null);
};

const handleTouchMove = (e: TouchEvent) => {
if (!isActive) return;

const currentX = e.touches[0].clientX;
const currentY = e.touches[0].clientY;
const deltaX = currentX - touchStartX.current;
const deltaY = currentY - touchStartY.current;
const absDeltaX = Math.abs(deltaX);
const absDeltaY = Math.abs(deltaY);

// 只有当水平滑动距离明显大于垂直滑动距离时,才进行视觉反馈
if (absDeltaX > absDeltaY && absDeltaX > 10) {
// 限制滑动距离,避免过度滑动
const maxSwipe = 200;
const limitedDeltaX = Math.max(-maxSwipe, Math.min(maxSwipe, deltaX));
setTranslateX(limitedDeltaX);

// 设置滑动方向
if (deltaX > 0) {
setSwipeDirection("right");
} else {
setSwipeDirection("left");
}
}
};

const handleTouchEnd = (e: TouchEvent) => {
touchEndX.current = e.changedTouches[0].clientX;
touchEndY.current = e.changedTouches[0].clientY;
isActive = false;
handleSwipe();
};

const handleSwipe = () => {
const distance = touchStartX.current - touchEndX.current;
if (distance > threshold && onSwipeLeft) {
onSwipeLeft();
} else if (distance < -threshold && onSwipeRight) {
onSwipeRight();
const deltaX = touchStartX.current - touchEndX.current;
const deltaY = touchStartY.current - touchEndY.current;
const absDeltaX = Math.abs(deltaX);
const absDeltaY = Math.abs(deltaY);

// 只有当水平滑动距离明显大于垂直滑动距离时,才触发翻页
if (absDeltaX > absDeltaY && absDeltaX > threshold) {
if (deltaX > 0 && onSwipeLeft) {
// 向左滑动,翻到下一页
triggerSwipeAnimation("left", onSwipeLeft);
} else if (deltaX < 0 && onSwipeRight) {
// 向右滑动,翻到上一页
triggerSwipeAnimation("right", onSwipeRight);
} else {
// 滑动距离不够,回弹
resetSwipeAnimation();
}
} else {
// 不是有效的滑动,回弹
resetSwipeAnimation();
}
};

const triggerSwipeAnimation = (
direction: "left" | "right",
callback: () => void
) => {
// 完成滑动动画,让旧页面滑出
const finalTranslate =
direction === "left" ? -window.innerWidth : window.innerWidth;
setIsTransitioning(true);
setTranslateX(finalTranslate);

// 等待动画完成(旧页面完全滑出)
setTimeout(() => {
// 先设置新内容从相反方向进入的位置(在内容更新之前)
const enterTranslate =
direction === "left" ? window.innerWidth : -window.innerWidth;
setTranslateX(enterTranslate);

// 增加 key 值,强制重新渲染
contentKeyRef.current += 1;

// 然后执行回调更新内容
// 使用 requestAnimationFrame 确保位置设置完成后再更新内容
requestAnimationFrame(() => {
callback();

// 等待新内容渲染完成后再滑入
setTimeout(() => {
setTranslateX(0);
setIsSwiping(false);
setIsTransitioning(false);
setSwipeDirection(null);
}, 50); // 给 React 一些时间完成渲染
});
}, 250); // 稍微延长一点时间,确保动画完全完成
};

const resetSwipeAnimation = () => {
// 回弹动画
setTranslateX(0);
setTimeout(() => {
setIsSwiping(false);
setSwipeDirection(null);
}, 300);
};

const containerElement = containerRef.current;
if (containerElement) {
containerElement.addEventListener("touchstart", handleTouchStart, {
passive: true,
});
containerElement.addEventListener("touchmove", handleTouchMove, {
passive: true,
});
containerElement.addEventListener("touchend", handleTouchEnd, {
passive: true,
});
Expand All @@ -51,14 +161,56 @@ export const SwipeContainer = ({
return () => {
if (containerElement) {
containerElement.removeEventListener("touchstart", handleTouchStart);
containerElement.removeEventListener("touchmove", handleTouchMove);
containerElement.removeEventListener("touchend", handleTouchEnd);
}
};
}, [threshold, onSwipeLeft, onSwipeRight]);

// 计算不透明度,滑动距离越大,当前页面越透明
const opacity =
isSwiping && Math.abs(translateX) > 20
? Math.max(0.3, 1 - Math.abs(translateX) / 300)
: 1;

// 计算阴影,滑动时显示阴影效果
const boxShadow =
isSwiping && Math.abs(translateX) > 20
? `0 ${Math.abs(translateX) / 10}px ${
Math.abs(translateX) / 5
}px rgba(0, 0, 0, ${Math.min(0.3, Math.abs(translateX) / 500)})`
: "none";

return (
<div ref={containerRef} className={className}>
{children}
<div
ref={containerRef}
className={`relative overflow-hidden ${className}`}
style={{ touchAction: "pan-y", ...style }}
>
<div
key={contentKeyRef.current}
ref={contentRef}
className="transition-all ease-out"
style={{
transitionDuration: isTransitioning ? "250ms" : "200ms",
transform: `translateX(${translateX}px)`,
opacity: isTransitioning ? 1 : opacity,
boxShadow: isTransitioning ? "none" : boxShadow,
willChange:
isSwiping || isTransitioning ? "transform, opacity" : "auto",
}}
>
{children}
</div>

{/* 滑动指示器 */}
{isSwiping && Math.abs(translateX) > 30 && (
<div className="absolute inset-0 pointer-events-none flex items-center justify-center z-10">
<div className="bg-black/50 dark:bg-white/50 backdrop-blur-sm rounded-full px-4 py-2 text-white dark:text-black text-sm font-medium">
{swipeDirection === "left" ? "下一页 →" : "← 上一页"}
</div>
</div>
)}
</div>
);
};
Loading