"use client"; import { AlertTriangle, CheckCircle2, Info, Loader2, X, XCircle } from "lucide-react"; import type { ComponentProps, ReactNode } from "react"; import { useEffect } from "react"; import { toast, Toaster } from "sonner"; import { cn } from "@/lib/utils"; import { MAP_COMPACT_RADIUS_CLASS_NAME, MAP_ICON_CELL_RADIUS_CLASS_NAME, MAP_READABLE_RADIUS_CLASS_NAME } from "./map-control-styles"; export type MapNoticeTone = "info" | "success" | "warning" | "error" | "loading"; export type MapNoticePosition = "top-center" | "top-right" | "bottom-left" | "bottom-right"; export type MapNoticeOptions = { id?: string | number; title?: string; message: string; tone?: MapNoticeTone; duration?: number; position?: MapNoticePosition; actionLabel?: string; onAction?: () => void; dismissible?: boolean; }; type SonnerPosition = NonNullable["position"]>; const DEFAULT_POSITION: MapNoticePosition = "top-center"; const positionClassNames: Record = { "top-center": "!left-1/2 !right-auto top-[84px] [translate:-50%_0]", "top-right": "top-[84px] right-[72px]", "bottom-left": "bottom-12 left-4", "bottom-right": "bottom-12 right-4" }; const sonnerPositions: Record = { "top-center": "top-center", "top-right": "top-right", "bottom-left": "bottom-left", "bottom-right": "bottom-right" }; const toneClassNames: Record = { info: "acrylic-panel text-slate-900", success: "acrylic-panel text-slate-900", warning: "acrylic-panel text-slate-900", error: "acrylic-panel text-slate-900", loading: "acrylic-panel text-slate-900" }; const iconClassNames: Record = { info: "text-blue-600", success: "text-green-600", warning: "text-orange-500", error: "text-red-500", loading: "text-blue-600" }; const noticeAccentClassNames: Record = { info: "bg-blue-600", success: "bg-green-500", warning: "bg-orange-500", error: "bg-red-500", loading: "bg-blue-600" }; const noticeIconSurfaceClassNames: Record = { info: "border-blue-100 bg-blue-50", success: "border-green-100 bg-green-50", warning: "border-orange-100 bg-orange-50", error: "border-red-100 bg-red-50", loading: "border-blue-100 bg-blue-50" }; export function MapToaster() { return ( ); } export function showMapNotice(options: MapNoticeOptions) { const tone = options.tone ?? "info"; const position = options.position ?? DEFAULT_POSITION; const duration = options.duration ?? (tone === "info" || tone === "success" ? 3200 : Infinity); const dismissible = options.dismissible ?? tone !== "loading"; return toast.custom( () => , { id: options.id, duration, position: sonnerPositions[position], dismissible, cancel: dismissible ? { label: , onClick: (event) => event.stopPropagation() } : undefined, classNames: { cancelButton: cn( "absolute right-2 top-2 z-10 grid h-7 w-7 place-items-center border border-transparent bg-transparent p-0 text-slate-400 transition hover:border-slate-200 hover:bg-slate-50 hover:text-slate-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-600/25", MAP_ICON_CELL_RADIUS_CLASS_NAME ) }, className: cn("!fixed !z-[60] !w-[min(420px,calc(100vw-24px))]", positionClassNames[position]) } ); } export function dismissMapNotice(id?: string | number) { toast.dismiss(id); } export function MapErrorNotice({ message, id = "map-error" }: { message: string; id?: string | number }) { useEffect(() => { showMapNotice({ id, tone: "error", title: "地图异常", message, duration: Infinity }); return () => dismissMapNotice(id); }, [id, message]); return null; } export function MapLoadingNotice({ message = "正在初始化地图...", title = "地图加载中", id = "map-loading" }: { message?: string; title?: string; id?: string | number; }) { useEffect(() => { showMapNotice({ id, tone: "loading", title, message, duration: Infinity, dismissible: false }); return () => dismissMapNotice(id); }, [id, message, title]); return null; } export type MapSourceStatus = "online" | "degraded" | "offline"; export type MapSourceStatusNoticeProps = { sourceName: string; status: MapSourceStatus; message?: string; id?: string | number; actionLabel?: string; onAction?: () => void; }; export function MapSourceStatusNotice({ sourceName, status, message, id = `map-source-${sourceName}`, actionLabel, onAction }: MapSourceStatusNoticeProps) { useEffect(() => { showMapNotice({ id, tone: getSourceStatusTone(status), title: getSourceStatusTitle(sourceName, status), message: message ?? getSourceStatusMessage(status), duration: status === "online" ? 2600 : Infinity, actionLabel, onAction }); return () => dismissMapNotice(id); }, [actionLabel, id, message, onAction, sourceName, status]); return null; } function MapNoticeContent({ title, message, tone = "info", actionLabel, onAction }: MapNoticeOptions) { const Icon = getNoticeIcon(tone); return ( : null} /> ); } function MapNoticeFrame({ tone, children }: { tone: MapNoticeTone; children: ReactNode }) { return (
); } function MapNoticeIcon({ tone, icon: Icon }: { tone: MapNoticeTone; icon: ReturnType }) { return ( ); } function MapNoticeBody({ title, message, action }: { title?: string; message: string; action?: ReactNode; }) { return ( {title ? {title} : null} {message} {action} ); } function MapNoticeAction({ label, onClick }: { label: string; onClick: () => void }) { return ( ); } function MapNoticeCloseIcon() { return ( <> 关闭通知