Files
36-character-flower/src/components/ui/notification-alert.tsx
JiaJun 23416df765 feat(game): 添加最大投注金额限制功能
- 在 UserStreakMessageData 接口中添加 maxBetPerNumber 字段
- 在 WebSocket 消息解析器中处理 max_bet_per_number 数据
- 实现游戏实时同步中最大投注金额的更新逻辑
- 将最大投注金额同步到游戏会话状态的仪表板数据
- 更新通知弹窗组件样式以支持当前文本颜色
- 在通知弹窗标题区域应用主题色调类名
2026-06-10 14:34:52 +08:00

216 lines
8.1 KiB
TypeScript

import {
CheckCircle2,
Info,
LoaderCircle,
TriangleAlert,
XCircle,
} from 'lucide-react'
import { motion } from 'motion/react'
import { createPortal } from 'react-dom'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
import { NOTIFICATION_EXIT_DURATION_MS } from '@/constants'
import { useNotificationStore } from '@/lib/notify'
import { cn } from '@/lib/utils'
const TONE_CLASS_BY_TYPE = {
error: {
alert:
'border-[#E37284]/38 bg-[linear-gradient(180deg,rgba(46,11,18,0.96),rgba(23,7,11,0.98))] shadow-[0_calc(var(--design-unit)*18)_calc(var(--design-unit)*44)_rgba(118,18,40,0.34)]',
glow: 'from-[#FF7D93]/20 via-[#FF7D93]/8 to-transparent',
icon: <XCircle className="h-design-18 w-design-18 text-[#FFB2BE]" />,
iconShell:
'border-[rgba(255,154,168,0.26)] bg-[linear-gradient(180deg,rgba(124,34,46,0.42),rgba(76,19,29,0.3))] shadow-[inset_0_1px_0_rgba(255,224,228,0.16)]',
line: 'bg-[#FF7D93]',
title: 'text-[#FFF5F7]',
},
info: {
alert:
'border-[rgba(114,226,243,0.34)] bg-[linear-gradient(180deg,rgba(5,30,43,0.96),rgba(4,17,26,0.98))] shadow-[0_calc(var(--design-unit)*18)_calc(var(--design-unit)*44)_rgba(9,78,108,0.34)]',
glow: 'from-[#6FEAFF]/20 via-[#6FEAFF]/8 to-transparent',
icon: <Info className="h-design-18 w-design-18 text-[#A5F3FF]" />,
iconShell:
'border-[rgba(121,228,238,0.24)] bg-[linear-gradient(180deg,rgba(17,85,98,0.38),rgba(10,53,63,0.28))] shadow-[inset_0_1px_0_rgba(227,252,255,0.14)]',
line: 'bg-[#73ECFF]',
title: 'text-[#F2FEFF]',
},
loading: {
alert:
'border-[rgba(114,226,243,0.34)] bg-[linear-gradient(180deg,rgba(5,30,43,0.96),rgba(4,17,26,0.98))] shadow-[0_calc(var(--design-unit)*18)_calc(var(--design-unit)*44)_rgba(9,78,108,0.34)]',
glow: 'from-[#6FEAFF]/20 via-[#6FEAFF]/8 to-transparent',
icon: (
<LoaderCircle className="h-design-18 w-design-18 animate-spin text-[#A5F3FF]" />
),
iconShell:
'border-[rgba(121,228,238,0.24)] bg-[linear-gradient(180deg,rgba(17,85,98,0.38),rgba(10,53,63,0.28))] shadow-[inset_0_1px_0_rgba(227,252,255,0.14)]',
line: 'bg-[#73ECFF]',
title: 'text-[#F2FEFF]',
},
success: {
alert:
'border-[rgba(121,229,171,0.34)] bg-[linear-gradient(180deg,rgba(8,36,29,0.96),rgba(5,18,14,0.98))] shadow-[0_calc(var(--design-unit)*18)_calc(var(--design-unit)*44)_rgba(12,89,53,0.34)]',
glow: 'from-[#73F0B0]/18 via-[#73F0B0]/7 to-transparent',
icon: <CheckCircle2 className="h-design-18 w-design-18 text-[#B8FFD8]" />,
iconShell:
'border-[rgba(127,236,177,0.24)] bg-[linear-gradient(180deg,rgba(24,98,66,0.38),rgba(15,60,40,0.28))] shadow-[inset_0_1px_0_rgba(232,255,242,0.14)]',
line: 'bg-[#73F0B0]',
title: 'text-[#F4FFF8]',
},
warning: {
alert:
'border-[rgba(239,197,103,0.36)] bg-[linear-gradient(180deg,rgba(48,31,10,0.96),rgba(25,17,7,0.98))] shadow-[0_calc(var(--design-unit)*18)_calc(var(--design-unit)*44)_rgba(109,72,12,0.34)]',
glow: 'from-[#FFD66E]/20 via-[#FFD66E]/8 to-transparent',
icon: <TriangleAlert className="h-design-18 w-design-18 text-[#FFE4A0]" />,
iconShell:
'border-[rgba(255,214,110,0.24)] bg-[linear-gradient(180deg,rgba(110,77,26,0.38),rgba(66,46,15,0.28))] shadow-[inset_0_1px_0_rgba(255,247,223,0.14)]',
line: 'bg-[#FFD66E]',
title: 'text-[#FFFAEF]',
},
} as const
export function AppNotificationAlert() {
const activeDialog = useNotificationStore((state) => state.activeDialog)
const closingDialogId = useNotificationStore((state) => state.closingDialogId)
if (!activeDialog || typeof document === 'undefined') {
return null
}
const tone = TONE_CLASS_BY_TYPE[activeDialog.type]
const isClosing = closingDialogId === activeDialog.id
const isMobileViewport =
typeof window !== 'undefined' ? window.innerWidth <= 768 : false
const motionState = isClosing ? 'closing' : 'visible'
const motionVariants = {
closing: {
filter: 'blur(1px)',
opacity: 0,
scale: 0.985,
y: -8,
transition: {
duration: NOTIFICATION_EXIT_DURATION_MS / 1000,
ease: [0.22, 1, 0.36, 1],
},
},
hidden: {
filter: 'blur(3px)',
opacity: 0,
scale: 0.94,
y: -18,
},
visible: {
filter: 'blur(0px)',
opacity: 1,
scale: 1,
y: 0,
transition: {
type: 'spring',
damping: 26,
mass: 0.72,
stiffness: 360,
},
},
} as const
return createPortal(
<div
className={cn(
'pointer-events-none fixed inset-x-0 z-50 flex justify-center',
isMobileViewport
? 'top-[calc(var(--design-unit)*38)] px-design-8'
: 'top-[calc(var(--design-unit)*52)] px-4',
)}
>
<motion.div
key={activeDialog.id}
initial="hidden"
animate={motionState}
variants={motionVariants}
className={cn(
'will-change-transform',
isMobileViewport
? 'w-[min(94vw,calc(var(--design-unit)*280))]'
: 'w-[min(92vw,calc(var(--design-unit)*468))]',
)}
>
<Alert
className={cn(
isMobileViewport
? 'relative origin-top flex w-full max-w-none items-center gap-design-8 overflow-hidden rounded-[calc(var(--design-unit)*10)] border px-design-10 py-design-8 text-left backdrop-blur-xl'
: 'relative origin-top flex w-full max-w-none items-center gap-design-12 overflow-hidden rounded-[calc(var(--design-unit)*14)] border px-design-14 py-design-12 text-left backdrop-blur-xl',
tone.alert,
)}
>
<div
aria-hidden="true"
className={cn(
'absolute inset-x-0 top-0 h-[1px] bg-gradient-to-r from-transparent via-white/24 to-transparent',
)}
/>
<div
aria-hidden="true"
className={cn(
isMobileViewport
? 'absolute top-design-8 bottom-design-8 left-design-7 w-[calc(var(--design-unit)*2.5)] rounded-full opacity-90 shadow-[0_0_calc(var(--design-unit)*10)_currentColor]'
: 'absolute top-design-10 bottom-design-10 left-design-10 w-[calc(var(--design-unit)*3)] rounded-full opacity-90 shadow-[0_0_calc(var(--design-unit)*14)_currentColor]',
tone.line,
)}
/>
<div
aria-hidden="true"
className={cn(
'absolute inset-0 bg-gradient-to-r opacity-100',
tone.glow,
)}
/>
<div
className={cn(
isMobileViewport
? 'relative z-10 flex h-design-28 w-design-28 shrink-0 items-center justify-center rounded-[calc(var(--design-unit)*8)] border'
: 'relative z-10 flex h-design-38 w-design-38 shrink-0 items-center justify-center rounded-[calc(var(--design-unit)*11)] border',
tone.iconShell,
)}
>
{tone.icon}
</div>
<div
className={cn(
'relative z-10 min-w-0 flex-1 flex-col justify-center',
isMobileViewport
? 'flex min-h-design-28'
: 'flex min-h-design-38',
tone.title,
)}
>
<AlertTitle
className={cn(
isMobileViewport
? 'text-design-11 font-semibold leading-[1.25] text-shadow-[0_1px_0_rgba(0,0,0,0.18)]'
: 'text-design-15 font-semibold leading-[1.25] text-shadow-[0_1px_0_rgba(0,0,0,0.18)]',
tone.title,
)}
>
{activeDialog.message}
</AlertTitle>
{activeDialog.description ? (
<AlertDescription
className={cn(
'whitespace-pre-line !text-current',
isMobileViewport
? 'pt-design-2 text-design-9 leading-[1.35]'
: 'pt-design-3 text-design-12 leading-[1.45]',
)}
>
{activeDialog.description}
</AlertDescription>
) : null}
</div>
</Alert>
</motion.div>
</div>,
document.body,
)
}