refactor(P1): 5 项代码质量改进 — 消除重复、拆分巨型组件、统一基础设施
Task 4: 统一 amap.ts 为完整 API 客户端 - 扩展 amap.ts 为统一客户端(amapFetch 8s 超时 + 错误处理) - 导出 searchPlaceText/searchPlaceAround/getInputTips/reverseGeocode/getTransitDirection - 精简 4 个 location route 为单行调用,blindboxPlanGen 删除 ~80 行内联 API 代码 Task 2: 抽取 ShareCardShell 消除三兄弟重复 - 新建 ShareCardShell.tsx 共享外框/背景/品牌头/QR 底部 - RestaurantShareCard 406→268 行,BlindboxShareCard 341→173 行,BlindboxPlanShareCard 277→159 行 Task 3: 拆分 BlindboxPlan.tsx (742→371 行) - 提取 planUtils.ts (guessCategory + formatDuration) - 提取 PoiSearchField / SortablePlanItem / PlanItemEditModal 三个独立组件 Task 1: 拆分 blindbox/[code]/page.tsx 上帝组件 (1300→509 行) - 提取 useBlindboxRoom / useBlindboxIdeas / useBlindboxPlan / useBlindboxDraw 四个 hooks - 提取 BlindboxPoolPhase / BlindboxRevealPhase 两个子组件 - 主页面仅保留 phase 协调 + hook 组装 + 子组件渲染 Task 5: 统一 SWR 数据获取层 - 新建 fetcher.ts (FetchError 携带 status,401 不重试) - 新建 useBlindboxRooms / useAchievements / useFavorites SWR hooks - useRoomPolling 改用共享 fetcher - blindbox 大厅/成就/个人中心页面删除手写 fetch 样板代码 - JWT 过期时自动弹出登录框而非反复重试
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import { Star, MapPin, Zap } from "lucide-react";
|
||||
import { QRCodeSVG } from "qrcode.react";
|
||||
import type { Restaurant, MatchType, SceneType } from "@/types";
|
||||
import { getSceneConfig } from "@/lib/sceneConfig";
|
||||
import ShareCardShell from "./ShareCardShell";
|
||||
import type { ShareCardTheme } from "./ShareCardShell";
|
||||
|
||||
export interface RestaurantShareData {
|
||||
type: "restaurant";
|
||||
@@ -12,6 +13,25 @@ export interface RestaurantShareData {
|
||||
scene?: SceneType;
|
||||
}
|
||||
|
||||
function buildTheme(isUnanimous: boolean): ShareCardTheme & { accentText: string; accentBg: string } {
|
||||
const accentFrom = isUnanimous ? "#059669" : "#b45309";
|
||||
const accentTo = isUnanimous ? "#34d399" : "#fbbf24";
|
||||
return {
|
||||
emoji: "⚡",
|
||||
tagline: "别说随便 · PANIC MODE",
|
||||
bgColor: "#08080a",
|
||||
gradientBorder: `linear-gradient(160deg, ${accentFrom}, ${accentTo}40, ${accentFrom}30)`,
|
||||
accentLine: `linear-gradient(to right, transparent, ${accentTo}30, transparent)`,
|
||||
glows: [
|
||||
{ top: -40, right: -30, width: 160, height: 160, color: `${accentFrom}25` },
|
||||
{ top: 9999, right: 9999, width: 120, height: 120, color: `${accentTo}12` },
|
||||
],
|
||||
qrFgColor: "#08080a",
|
||||
accentText: isUnanimous ? "#6ee7b7" : "#fcd34d",
|
||||
accentBg: isUnanimous ? "rgba(16, 185, 129, 0.12)" : "rgba(245, 158, 11, 0.12)",
|
||||
};
|
||||
}
|
||||
|
||||
export default function RestaurantShareCard({
|
||||
data,
|
||||
cardRef,
|
||||
@@ -26,381 +46,223 @@ export default function RestaurantShareCard({
|
||||
const { restaurant, matchType, matchLikes, userCount, scene } = data;
|
||||
const isUnanimous = matchType === "unanimous";
|
||||
const verb = getSceneConfig(scene ?? "eat").verb;
|
||||
const shareUrl =
|
||||
typeof window !== "undefined" ? window.location.origin : "nowhatever.app";
|
||||
const accentFrom = isUnanimous ? "#059669" : "#b45309";
|
||||
const theme = buildTheme(isUnanimous);
|
||||
const { accentText, accentBg } = theme;
|
||||
const accentTo = isUnanimous ? "#34d399" : "#fbbf24";
|
||||
const accentText = isUnanimous ? "#6ee7b7" : "#fcd34d";
|
||||
const accentBg = isUnanimous
|
||||
? "rgba(16, 185, 129, 0.12)"
|
||||
: "rgba(245, 158, 11, 0.12)";
|
||||
|
||||
// Fix the second glow position (bottom-left, not the placeholder)
|
||||
theme.glows[1] = { top: 9999, right: 9999, width: 120, height: 120, color: `${accentTo}12` };
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={cardRef}
|
||||
style={{
|
||||
width: 340,
|
||||
padding: 1.5,
|
||||
borderRadius: 20,
|
||||
background: `linear-gradient(160deg, ${accentFrom}, ${accentTo}40, ${accentFrom}30)`,
|
||||
fontFamily:
|
||||
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
||||
}}
|
||||
>
|
||||
<ShareCardShell theme={theme} cardRef={cardRef} bgDataUrl={bgDataUrl}>
|
||||
{/* Second glow override (bottom-left) */}
|
||||
<div
|
||||
style={{
|
||||
borderRadius: 18.5,
|
||||
background: "#08080a",
|
||||
position: "absolute",
|
||||
bottom: 60,
|
||||
left: -40,
|
||||
width: 120,
|
||||
height: 120,
|
||||
borderRadius: "50%",
|
||||
background: `radial-gradient(circle, ${accentTo}12, transparent 70%)`,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Hero section */}
|
||||
<div
|
||||
style={{
|
||||
padding: "24px 20px 20px",
|
||||
textAlign: "center",
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{/* Background image */}
|
||||
{bgDataUrl && (
|
||||
<img
|
||||
src={bgDataUrl}
|
||||
alt=""
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "cover",
|
||||
opacity: 0.12,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Decorative glows */}
|
||||
<div style={{ fontSize: 40, lineHeight: 1 }}>
|
||||
{isUnanimous ? "🎉" : "🏆"}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: -40,
|
||||
right: -30,
|
||||
width: 160,
|
||||
height: 160,
|
||||
borderRadius: "50%",
|
||||
background: `radial-gradient(circle, ${accentFrom}25, transparent 70%)`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 60,
|
||||
left: -40,
|
||||
width: 120,
|
||||
height: 120,
|
||||
borderRadius: "50%",
|
||||
background: `radial-gradient(circle, ${accentTo}12, transparent 70%)`,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Brand header */}
|
||||
<div
|
||||
style={{
|
||||
padding: "14px 20px 12px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
position: "relative",
|
||||
fontSize: 30,
|
||||
fontWeight: 900,
|
||||
color: "#ffffff",
|
||||
marginTop: 12,
|
||||
letterSpacing: "0.08em",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<span style={{ fontSize: 18 }}>⚡</span>
|
||||
<div>
|
||||
就去这{verb}!
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
marginTop: 12,
|
||||
padding: "6px 16px",
|
||||
borderRadius: 100,
|
||||
background: accentBg,
|
||||
border: `1px solid ${accentTo}20`,
|
||||
}}
|
||||
>
|
||||
{isUnanimous && (
|
||||
<Zap size={12} style={{ color: accentText, fill: accentText }} />
|
||||
)}
|
||||
<span style={{ fontSize: 11, fontWeight: 700, color: accentText }}>
|
||||
{isUnanimous
|
||||
? `默契度 100% · ${userCount}人全员一致`
|
||||
: `${matchLikes}/${userCount} 人选了这家`}
|
||||
</span>
|
||||
{isUnanimous && (
|
||||
<Zap size={12} style={{ color: accentText, fill: accentText }} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Restaurant card */}
|
||||
<div style={{ padding: "0 16px 16px" }}>
|
||||
<div
|
||||
style={{
|
||||
borderRadius: 14,
|
||||
background: "rgba(255,255,255,0.04)",
|
||||
border: "1px solid rgba(255,255,255,0.06)",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{imageDataUrl && (
|
||||
<img
|
||||
src={imageDataUrl}
|
||||
alt={restaurant.name}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: 150,
|
||||
objectFit: "cover",
|
||||
display: "block",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div style={{ padding: "14px 16px 16px" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "flex-start",
|
||||
justifyContent: "space-between",
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 13,
|
||||
fontSize: 17,
|
||||
fontWeight: 800,
|
||||
color: "#ffffff",
|
||||
letterSpacing: "0.02em",
|
||||
lineHeight: 1.3,
|
||||
}}
|
||||
>
|
||||
NoWhatever
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 9,
|
||||
fontWeight: 600,
|
||||
color: "rgba(255,255,255,0.35)",
|
||||
letterSpacing: "0.15em",
|
||||
marginTop: 1,
|
||||
}}
|
||||
>
|
||||
别说随便 · PANIC MODE
|
||||
{restaurant.name}
|
||||
</div>
|
||||
{restaurant.category && (
|
||||
<span
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
padding: "3px 8px",
|
||||
borderRadius: 100,
|
||||
background: accentBg,
|
||||
fontSize: 10,
|
||||
fontWeight: 600,
|
||||
color: accentText,
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{restaurant.category}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Thin accent line */}
|
||||
<div
|
||||
style={{
|
||||
height: 1,
|
||||
margin: "0 20px",
|
||||
background: `linear-gradient(to right, transparent, ${accentTo}30, transparent)`,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Hero section */}
|
||||
<div
|
||||
style={{
|
||||
padding: "24px 20px 20px",
|
||||
textAlign: "center",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: 40, lineHeight: 1 }}>
|
||||
{isUnanimous ? "🎉" : "🏆"}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 30,
|
||||
fontWeight: 900,
|
||||
color: "#ffffff",
|
||||
marginTop: 12,
|
||||
letterSpacing: "0.08em",
|
||||
}}
|
||||
>
|
||||
就去这{verb}!
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
marginTop: 12,
|
||||
padding: "6px 16px",
|
||||
borderRadius: 100,
|
||||
background: accentBg,
|
||||
border: `1px solid ${accentTo}20`,
|
||||
}}
|
||||
>
|
||||
{isUnanimous && (
|
||||
<Zap
|
||||
size={12}
|
||||
style={{ color: accentText, fill: accentText }}
|
||||
/>
|
||||
)}
|
||||
<span
|
||||
<div
|
||||
style={{
|
||||
fontSize: 11,
|
||||
fontWeight: 700,
|
||||
color: accentText,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 12,
|
||||
marginTop: 10,
|
||||
fontSize: 12,
|
||||
}}
|
||||
>
|
||||
{isUnanimous
|
||||
? `默契度 100% · ${userCount}人全员一致`
|
||||
: `${matchLikes}/${userCount} 人选了这家`}
|
||||
</span>
|
||||
{isUnanimous && (
|
||||
<Zap
|
||||
size={12}
|
||||
style={{ color: accentText, fill: accentText }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Restaurant card */}
|
||||
<div style={{ padding: "0 16px 16px" }}>
|
||||
<div
|
||||
style={{
|
||||
borderRadius: 14,
|
||||
background: "rgba(255,255,255,0.04)",
|
||||
border: "1px solid rgba(255,255,255,0.06)",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{imageDataUrl && (
|
||||
<img
|
||||
src={imageDataUrl}
|
||||
alt={restaurant.name}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: 150,
|
||||
objectFit: "cover",
|
||||
display: "block",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div style={{ padding: "14px 16px 16px" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "flex-start",
|
||||
justifyContent: "space-between",
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 17,
|
||||
fontWeight: 800,
|
||||
color: "#ffffff",
|
||||
lineHeight: 1.3,
|
||||
}}
|
||||
>
|
||||
{restaurant.name}
|
||||
</div>
|
||||
{restaurant.category && (
|
||||
<span
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
padding: "3px 8px",
|
||||
borderRadius: 100,
|
||||
background: accentBg,
|
||||
fontSize: 10,
|
||||
fontWeight: 600,
|
||||
color: accentText,
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{restaurant.category}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 12,
|
||||
marginTop: 10,
|
||||
fontSize: 12,
|
||||
}}
|
||||
>
|
||||
{restaurant.rating > 0 && (
|
||||
<span
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 3,
|
||||
color: "#e5e7eb",
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
<Star
|
||||
size={13}
|
||||
style={{ color: "#fbbf24", fill: "#fbbf24" }}
|
||||
/>
|
||||
{restaurant.rating}
|
||||
</span>
|
||||
)}
|
||||
{restaurant.price && restaurant.price !== "未知" && (
|
||||
<span style={{ fontWeight: 700, color: accentText }}>
|
||||
{restaurant.price}
|
||||
</span>
|
||||
)}
|
||||
{restaurant.distance && (
|
||||
<span
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 3,
|
||||
color: "#9ca3af",
|
||||
}}
|
||||
>
|
||||
<MapPin size={12} style={{ color: "#6b7280" }} />
|
||||
{restaurant.distance}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{restaurant.address && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: 8,
|
||||
fontSize: 11,
|
||||
color: "#6b7280",
|
||||
lineHeight: 1.5,
|
||||
}}
|
||||
>
|
||||
📍 {restaurant.address}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{restaurant.tag && (
|
||||
<div
|
||||
{restaurant.rating > 0 && (
|
||||
<span
|
||||
style={{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
gap: 4,
|
||||
marginTop: 10,
|
||||
alignItems: "center",
|
||||
gap: 3,
|
||||
color: "#e5e7eb",
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{restaurant.tag
|
||||
.split(",")
|
||||
.slice(0, 4)
|
||||
.map((t) => (
|
||||
<span
|
||||
key={t}
|
||||
style={{
|
||||
padding: "2px 8px",
|
||||
borderRadius: 4,
|
||||
background: "rgba(255,255,255,0.05)",
|
||||
fontSize: 10,
|
||||
fontWeight: 500,
|
||||
color: "#9ca3af",
|
||||
}}
|
||||
>
|
||||
{t.trim()}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<Star size={13} style={{ color: "#fbbf24", fill: "#fbbf24" }} />
|
||||
{restaurant.rating}
|
||||
</span>
|
||||
)}
|
||||
{restaurant.price && restaurant.price !== "未知" && (
|
||||
<span style={{ fontWeight: 700, color: accentText }}>
|
||||
{restaurant.price}
|
||||
</span>
|
||||
)}
|
||||
{restaurant.distance && (
|
||||
<span
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 3,
|
||||
color: "#9ca3af",
|
||||
}}
|
||||
>
|
||||
<MapPin size={12} style={{ color: "#6b7280" }} />
|
||||
{restaurant.distance}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* QR footer */}
|
||||
<div
|
||||
style={{
|
||||
padding: "14px 20px 16px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 14,
|
||||
borderTop: "1px solid rgba(255,255,255,0.04)",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
padding: 5,
|
||||
borderRadius: 8,
|
||||
background: "#ffffff",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<QRCodeSVG
|
||||
value={shareUrl}
|
||||
size={52}
|
||||
level="M"
|
||||
bgColor="#ffffff"
|
||||
fgColor="#08080a"
|
||||
/>
|
||||
</div>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 12,
|
||||
fontWeight: 700,
|
||||
color: "rgba(255,255,255,0.7)",
|
||||
}}
|
||||
>
|
||||
扫码一起「别说随便」
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 10,
|
||||
color: "rgba(255,255,255,0.25)",
|
||||
marginTop: 3,
|
||||
}}
|
||||
>
|
||||
{shareUrl.replace(/^https?:\/\//, "")}
|
||||
</div>
|
||||
{restaurant.address && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: 8,
|
||||
fontSize: 11,
|
||||
color: "#6b7280",
|
||||
lineHeight: 1.5,
|
||||
}}
|
||||
>
|
||||
📍 {restaurant.address}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{restaurant.tag && (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
gap: 4,
|
||||
marginTop: 10,
|
||||
}}
|
||||
>
|
||||
{restaurant.tag
|
||||
.split(",")
|
||||
.slice(0, 4)
|
||||
.map((t) => (
|
||||
<span
|
||||
key={t}
|
||||
style={{
|
||||
padding: "2px 8px",
|
||||
borderRadius: 4,
|
||||
background: "rgba(255,255,255,0.05)",
|
||||
fontSize: 10,
|
||||
fontWeight: 500,
|
||||
color: "#9ca3af",
|
||||
}}
|
||||
>
|
||||
{t.trim()}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ShareCardShell>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user