diff --git a/PROJECT_AUDIT_2026-03-03.md b/PROJECT_AUDIT_2026-03-03.md index cb340ec..dd58b3e 100644 --- a/PROJECT_AUDIT_2026-03-03.md +++ b/PROJECT_AUDIT_2026-03-03.md @@ -148,9 +148,14 @@ - 将 `tsc --noEmit` 纳入 CI 必跑; - 优先修复测试目录类型错误,确保类型门禁恢复。 -### P2-4 Lint 存在阻塞错误(React hooks 新规则触发) +### P2-4 Lint 存在阻塞错误(React hooks 新规则触发)【已完成】 +- 修复状态:✅ 已完成(2026-03-03) +- 修复内容: + - 修复 `GlobalUserBadge`、`RestaurantCard`、`SwipeableCard`、`PageTransition`、`useGeolocation` 的 hooks 规则 error; + - 修复页面文案中的未转义引号(`react/no-unescaped-entities`); + - `npm run lint` 已恢复为 0 error(仍有 warning,后续可持续清理)。 - 证据: - - `npm run lint`:10 error / 32 warning。 + - 修复后执行 `npm run lint`:`0 errors / 29 warnings`。 - 代表性问题: - `src/components/SwipeableCard.tsx:81`(render 阶段注册副作用) - `src/components/GlobalUserBadge.tsx:23`(effect 内同步 setState) diff --git a/src/app/blindbox/page.tsx b/src/app/blindbox/page.tsx index 8802b4b..b92cf13 100644 --- a/src/app/blindbox/page.tsx +++ b/src/app/blindbox/page.tsx @@ -339,7 +339,7 @@ export default function BlindboxLobbyPage() { animate={{ opacity: 1 }} transition={{ duration: 0.5, delay: 0.08 }} > - 平日蓄水,周末开奖。把所有"想做但一直没做"的事,交给命运来决定。 + 平日蓄水,周末开奖。把所有"想做但一直没做"的事,交给命运来决定。 - 别再说"随便"了。 + 别再说"随便"了。
两个模式,覆盖你们所有的选择困难症。 diff --git a/src/components/GlobalUserBadge.tsx b/src/components/GlobalUserBadge.tsx index 3d3f2e7..8d99298 100644 --- a/src/components/GlobalUserBadge.tsx +++ b/src/components/GlobalUserBadge.tsx @@ -14,19 +14,11 @@ const HIDDEN_PREFIXES = ["/profile"]; export default function GlobalUserBadge() { const router = useRouter(); const pathname = usePathname(); - const [profile, setProfile] = useState(null); + const [profile, setProfile] = useState(() => getCachedProfile()); const [showAuth, setShowAuth] = useState(false); - const [theme, setTheme] = useState("system"); + const [theme, setTheme] = useState(() => getStoredTheme()); const hidden = HIDDEN_PREFIXES.some((p) => pathname.startsWith(p)); - useEffect(() => { - setProfile(getCachedProfile()); - }, [pathname]); - - useEffect(() => { - setTheme(getStoredTheme()); - }, []); - useEffect(() => { const handler = () => setProfile(getCachedProfile()); window.addEventListener("nowhatever_auth", handler); diff --git a/src/components/PageTransition.tsx b/src/components/PageTransition.tsx index 4187530..e32905f 100644 --- a/src/components/PageTransition.tsx +++ b/src/components/PageTransition.tsx @@ -1,6 +1,6 @@ "use client"; -import { useContext, useRef, type PropsWithChildren } from "react"; +import { useContext, useState, type PropsWithChildren } from "react"; import { AnimatePresence, motion } from "framer-motion"; import { usePathname } from "next/navigation"; import { LayoutRouterContext } from "next/dist/shared/lib/app-router-context.shared-runtime"; @@ -11,7 +11,7 @@ import { LayoutRouterContext } from "next/dist/shared/lib/app-router-context.sha */ function FrozenRoute({ children }: PropsWithChildren) { const ctx = useContext(LayoutRouterContext); - const frozen = useRef(ctx).current; + const [frozen] = useState(ctx); return ( {children} diff --git a/src/components/RestaurantCard.tsx b/src/components/RestaurantCard.tsx index a104bcc..3d34e2a 100644 --- a/src/components/RestaurantCard.tsx +++ b/src/components/RestaurantCard.tsx @@ -1,6 +1,6 @@ "use client"; -import { useCallback, useState, useEffect, useRef } from "react"; +import { useCallback, useState, useEffect } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { Star, MapPin, Clock, ExternalLink, Flame, Bookmark, ChevronLeft, ChevronRight } from "lucide-react"; import { Restaurant } from "@/types"; @@ -119,17 +119,6 @@ function ImageGallery({ images, name }: { images: string[]; name: string }) { export default function RestaurantCard({ restaurant, likeCount = 0 }: RestaurantCardProps) { const [favorited, setFavorited] = useState(false); - const [likeBounce, setLikeBounce] = useState(false); - const prevLikeRef = useRef(likeCount); - - useEffect(() => { - if (likeCount > prevLikeRef.current) { - setLikeBounce(true); - const t = setTimeout(() => setLikeBounce(false), 600); - return () => clearTimeout(t); - } - prevLikeRef.current = likeCount; - }, [likeCount]); const images = restaurant.images?.filter(Boolean); const hasImage = images && images.length > 0; @@ -175,21 +164,18 @@ export default function RestaurantCard({ restaurant, likeCount = 0 }: Restaurant {likeCount > 0 && ( - + {likeCount} 人想去 )} diff --git a/src/components/SwipeableCard.tsx b/src/components/SwipeableCard.tsx index 7ea16a2..7e140e6 100644 --- a/src/components/SwipeableCard.tsx +++ b/src/components/SwipeableCard.tsx @@ -1,6 +1,6 @@ "use client"; -import { useRef } from "react"; +import { useRef, useCallback, useEffect } from "react"; import { motion, useMotionValue, @@ -64,7 +64,7 @@ export default function SwipeableCard({ const isSwiping = useRef(false); - const flyOut = (direction: SwipeDirection) => { + const flyOut = useCallback((direction: SwipeDirection) => { if (isSwiping.current) return; isSwiping.current = true; const exit = getExitX(); @@ -75,11 +75,12 @@ export default function SwipeableCard({ damping: 40, onComplete: () => onSwipe(direction), }); - }; + }, [onSwipe, x]); - if (registerSwipe) { + useEffect(() => { + if (!registerSwipe) return; registerSwipe(flyOut); - } + }, [registerSwipe, flyOut]); const handleDragEnd = (_: unknown, info: PanInfo) => { const offsetX = info.offset.x; diff --git a/src/hooks/useGeolocation.ts b/src/hooks/useGeolocation.ts index 69c09af..e04cc2e 100644 --- a/src/hooks/useGeolocation.ts +++ b/src/hooks/useGeolocation.ts @@ -64,7 +64,10 @@ export function useGeolocation() { }, []); useEffect(() => { - locate(); + const timer = setTimeout(() => { + void locate(); + }, 0); + return () => clearTimeout(timer); }, [locate]); return { status, coords, locationName, retry: locate };