修复 lint 阻塞错误并恢复门禁

This commit is contained in:
2026-03-03 12:25:03 +08:00
parent 52b87abee3
commit 45dbac1935
8 changed files with 28 additions and 41 deletions
+7 -2
View File
@@ -148,9 +148,14 @@
-`tsc --noEmit` 纳入 CI 必跑; -`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/SwipeableCard.tsx:81`render 阶段注册副作用)
- `src/components/GlobalUserBadge.tsx:23`effect 内同步 setState - `src/components/GlobalUserBadge.tsx:23`effect 内同步 setState
+1 -1
View File
@@ -339,7 +339,7 @@ export default function BlindboxLobbyPage() {
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
transition={{ duration: 0.5, delay: 0.08 }} transition={{ duration: 0.5, delay: 0.08 }}
> >
"想做但一直没做" ""
</motion.p> </motion.p>
<motion.div <motion.div
+1 -1
View File
@@ -38,7 +38,7 @@ export default function LandingPage() {
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
transition={{ delay: 0.2 }} transition={{ delay: 0.2 }}
> >
"随便" &quot;便&quot;
<br /> <br />
</motion.p> </motion.p>
+2 -10
View File
@@ -14,19 +14,11 @@ const HIDDEN_PREFIXES = ["/profile"];
export default function GlobalUserBadge() { export default function GlobalUserBadge() {
const router = useRouter(); const router = useRouter();
const pathname = usePathname(); const pathname = usePathname();
const [profile, setProfile] = useState<UserProfile | null>(null); const [profile, setProfile] = useState<UserProfile | null>(() => getCachedProfile());
const [showAuth, setShowAuth] = useState(false); const [showAuth, setShowAuth] = useState(false);
const [theme, setTheme] = useState<Theme>("system"); const [theme, setTheme] = useState<Theme>(() => getStoredTheme());
const hidden = HIDDEN_PREFIXES.some((p) => pathname.startsWith(p)); const hidden = HIDDEN_PREFIXES.some((p) => pathname.startsWith(p));
useEffect(() => {
setProfile(getCachedProfile());
}, [pathname]);
useEffect(() => {
setTheme(getStoredTheme());
}, []);
useEffect(() => { useEffect(() => {
const handler = () => setProfile(getCachedProfile()); const handler = () => setProfile(getCachedProfile());
window.addEventListener("nowhatever_auth", handler); window.addEventListener("nowhatever_auth", handler);
+2 -2
View File
@@ -1,6 +1,6 @@
"use client"; "use client";
import { useContext, useRef, type PropsWithChildren } from "react"; import { useContext, useState, type PropsWithChildren } from "react";
import { AnimatePresence, motion } from "framer-motion"; import { AnimatePresence, motion } from "framer-motion";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { LayoutRouterContext } from "next/dist/shared/lib/app-router-context.shared-runtime"; 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) { function FrozenRoute({ children }: PropsWithChildren) {
const ctx = useContext(LayoutRouterContext); const ctx = useContext(LayoutRouterContext);
const frozen = useRef(ctx).current; const [frozen] = useState(ctx);
return ( return (
<LayoutRouterContext.Provider value={frozen}> <LayoutRouterContext.Provider value={frozen}>
{children} {children}
+5 -19
View File
@@ -1,6 +1,6 @@
"use client"; "use client";
import { useCallback, useState, useEffect, useRef } from "react"; import { useCallback, useState, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
import { Star, MapPin, Clock, ExternalLink, Flame, Bookmark, ChevronLeft, ChevronRight } from "lucide-react"; import { Star, MapPin, Clock, ExternalLink, Flame, Bookmark, ChevronLeft, ChevronRight } from "lucide-react";
import { Restaurant } from "@/types"; import { Restaurant } from "@/types";
@@ -119,17 +119,6 @@ function ImageGallery({ images, name }: { images: string[]; name: string }) {
export default function RestaurantCard({ restaurant, likeCount = 0 }: RestaurantCardProps) { export default function RestaurantCard({ restaurant, likeCount = 0 }: RestaurantCardProps) {
const [favorited, setFavorited] = useState(false); 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 images = restaurant.images?.filter(Boolean);
const hasImage = images && images.length > 0; const hasImage = images && images.length > 0;
@@ -175,21 +164,18 @@ export default function RestaurantCard({ restaurant, likeCount = 0 }: Restaurant
<AnimatePresence> <AnimatePresence>
{likeCount > 0 && ( {likeCount > 0 && (
<motion.span <motion.span
key="like-badge" key={`like-badge-${likeCount}`}
className="flex items-center gap-0.5 rounded-full bg-rose-500/90 px-2 py-0.5 text-xs font-semibold text-white shadow-sm backdrop-blur-sm" className="flex items-center gap-0.5 rounded-full bg-rose-500/90 px-2 py-0.5 text-xs font-semibold text-white shadow-sm backdrop-blur-sm"
initial={{ opacity: 0, scale: 0.5, x: -8 }} initial={{ opacity: 0, scale: 0.5, x: -8 }}
animate={{ animate={{
opacity: 1, opacity: 1,
scale: likeBounce ? [1, 1.3, 1] : 1, scale: [1, 1.3, 1],
x: 0, x: 0,
}} }}
exit={{ opacity: 0, scale: 0.5 }} exit={{ opacity: 0, scale: 0.5 }}
transition={likeBounce transition={{ scale: { duration: 0.4, ease: "easeInOut" }, default: { type: "spring", stiffness: 400, damping: 20 } }}
? { scale: { duration: 0.4, ease: "easeInOut" }, default: { type: "spring", stiffness: 400, damping: 20 } }
: { type: "spring", stiffness: 400, damping: 20 }
}
> >
<Flame size={11} className={likeBounce ? "animate-pulse" : ""} /> <Flame size={11} className="animate-pulse" />
{likeCount} {likeCount}
</motion.span> </motion.span>
)} )}
+6 -5
View File
@@ -1,6 +1,6 @@
"use client"; "use client";
import { useRef } from "react"; import { useRef, useCallback, useEffect } from "react";
import { import {
motion, motion,
useMotionValue, useMotionValue,
@@ -64,7 +64,7 @@ export default function SwipeableCard({
const isSwiping = useRef(false); const isSwiping = useRef(false);
const flyOut = (direction: SwipeDirection) => { const flyOut = useCallback((direction: SwipeDirection) => {
if (isSwiping.current) return; if (isSwiping.current) return;
isSwiping.current = true; isSwiping.current = true;
const exit = getExitX(); const exit = getExitX();
@@ -75,11 +75,12 @@ export default function SwipeableCard({
damping: 40, damping: 40,
onComplete: () => onSwipe(direction), onComplete: () => onSwipe(direction),
}); });
}; }, [onSwipe, x]);
if (registerSwipe) { useEffect(() => {
if (!registerSwipe) return;
registerSwipe(flyOut); registerSwipe(flyOut);
} }, [registerSwipe, flyOut]);
const handleDragEnd = (_: unknown, info: PanInfo) => { const handleDragEnd = (_: unknown, info: PanInfo) => {
const offsetX = info.offset.x; const offsetX = info.offset.x;
+4 -1
View File
@@ -64,7 +64,10 @@ export function useGeolocation() {
}, []); }, []);
useEffect(() => { useEffect(() => {
locate(); const timer = setTimeout(() => {
void locate();
}, 0);
return () => clearTimeout(timer);
}, [locate]); }, [locate]);
return { status, coords, locationName, retry: locate }; return { status, coords, locationName, retry: locate };