feat: 撤回滑动功能,按钮移至进度条旁

- 新增 POST /api/room/[id]/undo 端点,撤回 like 和 swipeCount
- 进度条右侧显示"↩ 撤回"按钮,可一直撤回到第一张
- ActionButtons 恢复干净的两按钮布局,避免误触
This commit is contained in:
2026-02-24 17:48:47 +08:00
parent 1b06f4fc0e
commit 48e74c03e6
2 changed files with 92 additions and 3 deletions
+57
View File
@@ -0,0 +1,57 @@
import { NextResponse } from "next/server";
import { atomicUpdateRoom } from "@/lib/store";
export async function POST(
req: Request,
{ params }: { params: Promise<{ id: string }> },
) {
const { id } = await params;
try {
const { userId, restaurantId } = await req.json();
if (!userId || restaurantId == null) {
return NextResponse.json(
{ error: "userId and restaurantId are required" },
{ status: 400 },
);
}
const rid = String(restaurantId);
const updated = await atomicUpdateRoom(id, (data) => {
if (data.likes[rid]) {
data.likes[rid] = data.likes[rid].filter((u) => u !== userId);
if (data.likes[rid].length === 0) {
delete data.likes[rid];
}
}
if (data.match === rid) {
data.match = null;
}
const count = data.swipeCounts[userId] ?? 0;
if (count > 0) {
data.swipeCounts[userId] = count - 1;
}
return data;
});
if (!updated) {
return NextResponse.json(
{ error: "房间不存在或已过期" },
{ status: 404 },
);
}
return NextResponse.json({ ok: true });
} catch (e) {
console.error("Failed to undo swipe:", e);
return NextResponse.json(
{ error: "撤回失败" },
{ status: 500 },
);
}
}
+35 -3
View File
@@ -7,7 +7,7 @@ import ActionButtons from "./ActionButtons";
import MatchResult from "./MatchResult";
import SwipeGuide from "./SwipeGuide";
import { Restaurant, SwipeDirection, MatchType } from "@/types";
import { Heart } from "lucide-react";
import { Heart, Undo2 } from "lucide-react";
interface SwipeDeckProps {
restaurants: Restaurant[];
@@ -38,6 +38,7 @@ export default function SwipeDeck({
const [resetting, setResetting] = useState(false);
const [bubble, setBubble] = useState("");
const [guideVisible, setGuideVisible] = useState(true);
const [swipeHistory, setSwipeHistory] = useState<string[]>([]);
const swipeFnRef = useRef<((direction: SwipeDirection) => void) | null>(null);
const swipingRef = useRef(false);
const prevLikeCounts = useRef<Record<string, number>>({});
@@ -104,6 +105,7 @@ export default function SwipeDeck({
const action = direction === "right" ? "like" : "nope";
sendSwipe(current.id, action);
setSwipeHistory((h) => [...h, current.id]);
const nextIndex = currentIndex + 1;
setCurrentIndex(nextIndex);
swipeFnRef.current = null;
@@ -122,6 +124,27 @@ export default function SwipeDeck({
[],
);
const handleUndo = useCallback(async () => {
if (swipeHistory.length === 0 || currentIndex === 0) return;
const lastRid = swipeHistory[swipeHistory.length - 1];
try {
await fetch(`/api/room/${roomId}/undo`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ userId, restaurantId: lastRid }),
});
} catch {
// Best-effort
}
setSwipeHistory((h) => h.slice(0, -1));
setCurrentIndex((i) => i - 1);
setLocalMatchId(null);
swipeFnRef.current = null;
}, [swipeHistory, currentIndex, roomId, userId]);
const handleReset = useCallback(async () => {
setResetting(true);
try {
@@ -129,6 +152,7 @@ export default function SwipeDeck({
setCurrentIndex(0);
setShowMatch(false);
setLocalMatchId(null);
setSwipeHistory([]);
prevLikeCounts.current = {};
} finally {
setResetting(false);
@@ -147,8 +171,8 @@ export default function SwipeDeck({
return (
<>
{!allSwiped && !resolvedMatchId && (
<div className="flex items-center justify-center gap-2 px-4 pb-1">
<div className="h-1 flex-1 max-w-sm overflow-hidden rounded-full bg-zinc-100">
<div className="mx-auto flex w-full max-w-sm items-center gap-2 px-4 pb-1">
<div className="h-1 flex-1 overflow-hidden rounded-full bg-zinc-100">
<motion.div
className="h-full rounded-full bg-emerald-400"
initial={{ width: 0 }}
@@ -161,6 +185,14 @@ export default function SwipeDeck({
<span className="shrink-0 text-[11px] tabular-nums text-zinc-400">
{currentIndex}/{restaurants.length}
</span>
<button
onClick={handleUndo}
disabled={currentIndex === 0}
className="flex shrink-0 items-center gap-0.5 rounded-full px-1.5 py-0.5 text-[11px] font-medium text-amber-500 transition-colors active:bg-amber-50 disabled:opacity-0"
>
<Undo2 size={12} />
</button>
</div>
)}