feat: 接入全站图片资源 + 修复卡片滑动与房间轮询问题

图片资源接入:
- OG/Twitter 社交分享元数据 (og-image.png)
- 错误页插画替换图标 (error-robot.png)
- EmptyState 组件支持 image prop,空状态页面接入插画
- 餐厅图片 fallback 改用 restaurant-fallback.png
- 极速救场/周末契约页面添加 hero 装饰图
- 分享卡片添加背景图层 (share-bg-*.png),通过 base64 预加载
- 更新 App 图标 (apple-touch-icon, icon-192/512)

Bug 修复:
- SwipeDeck: swipe action 从 "nope" 改为 "pass",匹配 API 预期
- SwipeDeck: 用 ref 读取 currentIndex 避免竞态重置(本地滑动后
  被服务端旧 swipeCounts 立即清零)
- SwipeDeck: 卡片 key 加入 isTop 标识,强制 remount 解决
  framer-motion drag 手势在 isTop 切换时不重新初始化的问题
- SwipeableCard: initial 统一为背景位置,确保晋升为顶部卡片时
  有一致的放大动画
- useRoomPolling: roomId 为空时跳过 SWR 和 EventSource
- room page: joinRoom 前 guard roomId,消除退房时 404
- layout: 添加 metadataBase 消除 Next.js OG 图片警告
This commit is contained in:
2026-02-27 16:08:38 +08:00
parent 4073be9066
commit 76349f0dcf
30 changed files with 167 additions and 36 deletions
+2
View File
@@ -193,6 +193,7 @@ export default function AchievementsPage() {
) : decisions.length === 0 ? (
<EmptyState
icon={ClipboardList}
image="/empty-no-record.png"
title="还没有决策记录"
subtitle="使用极速救场后会在这里记录"
color="amber"
@@ -254,6 +255,7 @@ export default function AchievementsPage() {
) : contracts.length === 0 ? (
<EmptyState
icon={BarChart3}
image="/empty-no-room.png"
title="还没有契约记录"
subtitle="完成或过期的契约会在这里显示"
color="purple"
+10 -1
View File
@@ -167,11 +167,20 @@ export default function BlindboxLobbyPage() {
</div>
</motion.div>
<motion.img
src="/blindbox-hero.png"
alt="周末契约"
className="mt-3 h-28 w-auto"
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5, delay: 0.05 }}
/>
<motion.p
className="mt-2 max-w-xs text-center text-xs leading-relaxed text-muted"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5, delay: 0.05 }}
transition={{ duration: 0.5, delay: 0.08 }}
>
"想做但一直没做"
</motion.p>
+6 -7
View File
@@ -2,7 +2,7 @@
import { useEffect } from "react";
import { motion } from "framer-motion";
import { AlertTriangle, RotateCcw, Home } from "lucide-react";
import { RotateCcw, Home } from "lucide-react";
import Button from "@/components/Button";
export default function Error({
@@ -23,14 +23,13 @@ export default function Error({
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
>
<motion.div
className="relative flex h-20 w-20 items-center justify-center"
<motion.img
src="/error-robot.png"
alt="错误"
className="h-28 w-28"
animate={{ y: [0, -4, 0] }}
transition={{ duration: 2.5, repeat: Infinity, ease: "easeInOut" }}
>
<div className="absolute inset-0 rounded-2xl bg-rose-500/15 blur-lg" />
<AlertTriangle size={36} className="relative text-rose-400/80" strokeWidth={1.5} />
</motion.div>
/>
<h1 className="mt-6 text-xl font-bold text-heading"></h1>
<p className="mt-2 max-w-xs text-sm text-muted">
+1 -1
View File
@@ -17,7 +17,7 @@ export default function GlobalError({
<html lang="zh-CN">
<body style={{ margin: 0, fontFamily: "system-ui, sans-serif", background: "#0a0a0a", color: "#e5e5e5" }}>
<div style={{ display: "flex", minHeight: "100dvh", flexDirection: "column", alignItems: "center", justifyContent: "center", padding: "1.5rem" }}>
<div style={{ fontSize: "3rem" }}></div>
<img src="/error-robot.png" alt="错误" style={{ width: 120, height: 120 }} />
<h1 style={{ marginTop: "1.5rem", fontSize: "1.25rem", fontWeight: 700 }}></h1>
<p style={{ marginTop: "0.5rem", fontSize: "0.875rem", color: "#a3a3a3", textAlign: "center" }}>
+12
View File
@@ -12,9 +12,21 @@ const geistSans = Geist({
});
export const metadata: Metadata = {
metadataBase: new URL(process.env.NEXT_PUBLIC_BASE_URL || "https://nowhatever.app"),
title: "NoWhatever — 别说随便",
description: "像 Tinder 一样滑卡片,和朋友一起决定去哪吃!",
referrer: "no-referrer",
openGraph: {
title: "NoWhatever — 别说随便",
description: "像 Tinder 一样滑卡片,和朋友一起决定去哪吃!",
images: [{ url: "/og-image.png", width: 1200, height: 630 }],
},
twitter: {
card: "summary_large_image",
title: "NoWhatever — 别说随便",
description: "像 Tinder 一样滑卡片,和朋友一起决定去哪吃!",
images: ["/og-image.png"],
},
};
export const viewport: Viewport = {
+10 -1
View File
@@ -214,11 +214,20 @@ export default function PanicPage() {
</div>
</motion.div>
<motion.img
src="/panic-hero.png"
alt="极速救场"
className="mt-3 h-28 w-auto"
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5, delay: 0.05 }}
/>
<motion.p
className="mt-2 max-w-xs text-center text-xs leading-relaxed text-muted"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5, delay: 0.05 }}
transition={{ duration: 0.5, delay: 0.08 }}
>
{sceneConfig.subtitle}
</motion.p>
+1
View File
@@ -31,6 +31,7 @@ export default function RoomPage() {
} = useRoomPolling(roomId);
useEffect(() => {
if (!roomId) return;
const id = getUserId();
setUserId(id);