- 在 UserStreakMessageData 接口中添加 maxBetPerNumber 字段 - 在 WebSocket 消息解析器中处理 max_bet_per_number 数据 - 实现游戏实时同步中最大投注金额的更新逻辑 - 将最大投注金额同步到游戏会话状态的仪表板数据 - 更新通知弹窗组件样式以支持当前文本颜色 - 在通知弹窗标题区域应用主题色调类名
216 lines
8.1 KiB
TypeScript
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,
|
|
)
|
|
}
|