feat(modal): 添加居中模态框组件及游戏相关弹窗功能
- 新增 CenterModal 组件,支持标题、关闭按钮、背景图片等功能 - 添加键盘 ESC 键关闭模态框和页面滚动锁定功能 - 创建桌面端登录、注册和用户信息三个业务模态框 - 集成 lottie-web 动画库并创建 LottiePlayer 组件 - 在样式文件中添加模态框相关的 CSS 类和输入框样式 - 更新桌面入口文件集成登录和用户信息模态框 - 调整桌面控制组件间距和样式配置
@@ -32,6 +32,7 @@
|
||||
"dayjs": "^1.11.20",
|
||||
"i18next": "^26.0.5",
|
||||
"ky": "^2.0.1",
|
||||
"lottie-web": "^5.13.0",
|
||||
"lucide-react": "^1.9.0",
|
||||
"motion": "^12.38.0",
|
||||
"react": "^19.2.4",
|
||||
|
||||
8
pnpm-lock.yaml
generated
@@ -29,6 +29,9 @@ importers:
|
||||
ky:
|
||||
specifier: ^2.0.1
|
||||
version: 2.0.1
|
||||
lottie-web:
|
||||
specifier: ^5.13.0
|
||||
version: 5.13.0
|
||||
lucide-react:
|
||||
specifier: ^1.9.0
|
||||
version: 1.9.0(react@19.2.5)
|
||||
@@ -1565,6 +1568,9 @@ packages:
|
||||
resolution: {integrity: sha512-Ajzxb8CM6WAnFjgiloPsI3bF+WCxcvhdIG3KNA2KN962+tdBsHcuQ4k4qX/EcS/2CRkcc0iAkR956Nib6aXU/Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
lottie-web@5.13.0:
|
||||
resolution: {integrity: sha512-+gfBXl6sxXMPe8tKQm7qzLnUy5DUPJPKIyRHwtpCpyUEYjHYRJC/5gjUvdkuO2c3JllrPtHXH5UJJK8LRYl5yQ==}
|
||||
|
||||
lru-cache@5.1.1:
|
||||
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
||||
|
||||
@@ -3387,6 +3393,8 @@ snapshots:
|
||||
|
||||
longest@2.0.1: {}
|
||||
|
||||
lottie-web@5.13.0: {}
|
||||
|
||||
lru-cache@5.1.1:
|
||||
dependencies:
|
||||
yallist: 3.1.1
|
||||
|
||||
537
src/assets/lottie/test.json
Normal file
BIN
src/assets/system/login-bg.webp
Normal file
|
After Width: | Height: | Size: 267 KiB |
BIN
src/assets/system/login-text.webp
Normal file
|
After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 19 KiB |
BIN
src/assets/system/modal-normal-bg.png
Normal file
|
After Width: | Height: | Size: 546 KiB |
BIN
src/assets/system/right.webp
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
src/assets/system/userInfo-bg.webp
Normal file
|
After Width: | Height: | Size: 446 KiB |
124
src/components/center-modal.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
import { type ReactNode, useEffect } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import modalBg from '@/assets/system/modal-bg.webp'
|
||||
import modalClose from '@/assets/system/modal-close.webp'
|
||||
import modalNormalBg from '@/assets/system/modal-normal-bg.png'
|
||||
import { cn } from '@/lib/untils'
|
||||
|
||||
interface CenterModalProps {
|
||||
open: boolean
|
||||
onClose?: () => void
|
||||
title?: ReactNode
|
||||
titleAlign?: 'left' | 'center'
|
||||
isNormalBg?: boolean
|
||||
children?: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
const MODAL_HEADER_HEIGHT = 'calc(100% * 80 / 640)'
|
||||
|
||||
export function CenterModal({
|
||||
open,
|
||||
onClose,
|
||||
title,
|
||||
titleAlign = 'left',
|
||||
isNormalBg = false,
|
||||
children,
|
||||
className,
|
||||
}: CenterModalProps) {
|
||||
const handleClose = () => {
|
||||
onClose?.()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !onClose || typeof document === 'undefined') {
|
||||
return
|
||||
}
|
||||
|
||||
const previousOverflow = document.body.style.overflow
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
|
||||
document.body.style.overflow = 'hidden'
|
||||
window.addEventListener('keydown', handleKeyDown)
|
||||
|
||||
return () => {
|
||||
document.body.style.overflow = previousOverflow
|
||||
window.removeEventListener('keydown', handleKeyDown)
|
||||
}
|
||||
}, [open, onClose])
|
||||
|
||||
if (!open || typeof document === 'undefined') {
|
||||
return null
|
||||
}
|
||||
|
||||
return createPortal(
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-slate-950/72 backdrop-blur-sm">
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={typeof title === 'string' ? title : 'Modal'}
|
||||
className={cn(
|
||||
'relative flex h-design-640 w-design-720 flex-col overflow-hidden rounded-[calc(var(--design-unit)*28)] px-design-20 text-white',
|
||||
className,
|
||||
)}
|
||||
style={{
|
||||
backgroundImage: `url(${isNormalBg ? modalNormalBg : modalBg})`,
|
||||
backgroundPosition: 'center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundSize: '100% 100%',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="relative w-full shrink-0 mt-design-15 px-design-20"
|
||||
style={{ height: MODAL_HEADER_HEIGHT }}
|
||||
>
|
||||
{title ? (
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-full w-full items-center text-design-32 font-semibold tracking-[0.08em] text-cyan-50 sm:text-design-38',
|
||||
titleAlign === 'center'
|
||||
? 'justify-center text-center'
|
||||
: 'justify-start text-left',
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{onClose ? (
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Close modal"
|
||||
onClick={handleClose}
|
||||
className={cn(
|
||||
'absolute top-1/2 inline-flex h-design-60 w-design-60 -translate-y-1/2 items-center justify-center rounded-full transition hover:scale-105 active:scale-95',
|
||||
isNormalBg ? 'right-5' : 'right-10',
|
||||
)}
|
||||
>
|
||||
<img
|
||||
src={modalClose}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className="modal-close-glow relative z-10 h-design-60 w-design-48 object-contain cursor-pointer"
|
||||
/>
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'w-full min-h-0 flex-1 overflow-y-auto',
|
||||
title ? '' : 'pt-design-24',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
)
|
||||
}
|
||||
290
src/components/lottie-player.tsx
Normal file
@@ -0,0 +1,290 @@
|
||||
import lottie, {
|
||||
type AnimationConfigWithData,
|
||||
type AnimationConfigWithPath,
|
||||
type AnimationDirection,
|
||||
type AnimationEventCallback,
|
||||
type AnimationEvents,
|
||||
type AnimationItem,
|
||||
type AnimationSegment,
|
||||
type CanvasRendererConfig,
|
||||
type HTMLRendererConfig,
|
||||
type RendererType,
|
||||
type SVGRendererConfig,
|
||||
} from 'lottie-web'
|
||||
import {
|
||||
forwardRef,
|
||||
type HTMLAttributes,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
} from 'react'
|
||||
import { cn } from '@/lib/untils'
|
||||
|
||||
type LottieRendererSettings =
|
||||
| SVGRendererConfig
|
||||
| CanvasRendererConfig
|
||||
| HTMLRendererConfig
|
||||
|
||||
export interface LottiePlayerHandle {
|
||||
getInstance: () => AnimationItem | null
|
||||
play: () => void
|
||||
pause: () => void
|
||||
stop: () => void
|
||||
destroy: () => void
|
||||
setSpeed: (speed: number) => void
|
||||
setDirection: (direction: AnimationDirection) => void
|
||||
goToAndPlay: (value: number | string, isFrame?: boolean) => void
|
||||
goToAndStop: (value: number | string, isFrame?: boolean) => void
|
||||
}
|
||||
|
||||
export interface LottiePlayerProps
|
||||
extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
|
||||
animationData?: AnimationConfigWithData['animationData']
|
||||
path?: AnimationConfigWithPath['path']
|
||||
renderer?: RendererType
|
||||
loop?: boolean | number
|
||||
autoplay?: boolean
|
||||
isPaused?: boolean
|
||||
speed?: number
|
||||
direction?: AnimationDirection
|
||||
initialSegment?: AnimationSegment
|
||||
segments?: AnimationSegment | AnimationSegment[]
|
||||
forceSegments?: boolean
|
||||
name?: string
|
||||
assetsPath?: string
|
||||
rendererSettings?: LottieRendererSettings
|
||||
onComplete?: AnimationEventCallback<AnimationEvents['complete']>
|
||||
onLoopComplete?: AnimationEventCallback<AnimationEvents['loopComplete']>
|
||||
onEnterFrame?: AnimationEventCallback<AnimationEvents['enterFrame']>
|
||||
onDOMLoaded?: AnimationEventCallback<AnimationEvents['DOMLoaded']>
|
||||
onDataReady?: AnimationEventCallback<AnimationEvents['data_ready']>
|
||||
onDataFailed?: AnimationEventCallback<AnimationEvents['data_failed']>
|
||||
}
|
||||
|
||||
export const LottiePlayer = forwardRef<LottiePlayerHandle, LottiePlayerProps>(
|
||||
function LottiePlayer(
|
||||
{
|
||||
animationData,
|
||||
path,
|
||||
renderer = 'svg',
|
||||
loop = true,
|
||||
autoplay = true,
|
||||
isPaused = false,
|
||||
speed = 1,
|
||||
direction = 1,
|
||||
initialSegment,
|
||||
segments,
|
||||
forceSegments = true,
|
||||
name,
|
||||
assetsPath,
|
||||
rendererSettings,
|
||||
onComplete,
|
||||
onLoopComplete,
|
||||
onEnterFrame,
|
||||
onDOMLoaded,
|
||||
onDataReady,
|
||||
onDataFailed,
|
||||
className,
|
||||
...divProps
|
||||
},
|
||||
ref,
|
||||
) {
|
||||
const containerRef = useRef<HTMLDivElement | null>(null)
|
||||
const animationRef = useRef<AnimationItem | null>(null)
|
||||
const hasSyncedPauseStateRef = useRef(false)
|
||||
const loopRef = useRef(loop)
|
||||
const autoplayRef = useRef(autoplay)
|
||||
const speedRef = useRef(speed)
|
||||
const directionRef = useRef(direction)
|
||||
const segmentsRef = useRef(segments)
|
||||
const forceSegmentsRef = useRef(forceSegments)
|
||||
const isPausedRef = useRef(isPaused)
|
||||
const completeCallbackRef = useRef(onComplete)
|
||||
const loopCompleteCallbackRef = useRef(onLoopComplete)
|
||||
const enterFrameCallbackRef = useRef(onEnterFrame)
|
||||
const domLoadedCallbackRef = useRef(onDOMLoaded)
|
||||
const dataReadyCallbackRef = useRef(onDataReady)
|
||||
const dataFailedCallbackRef = useRef(onDataFailed)
|
||||
|
||||
loopRef.current = loop
|
||||
autoplayRef.current = autoplay
|
||||
speedRef.current = speed
|
||||
directionRef.current = direction
|
||||
segmentsRef.current = segments
|
||||
forceSegmentsRef.current = forceSegments
|
||||
isPausedRef.current = isPaused
|
||||
completeCallbackRef.current = onComplete
|
||||
loopCompleteCallbackRef.current = onLoopComplete
|
||||
enterFrameCallbackRef.current = onEnterFrame
|
||||
domLoadedCallbackRef.current = onDOMLoaded
|
||||
dataReadyCallbackRef.current = onDataReady
|
||||
dataFailedCallbackRef.current = onDataFailed
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
getInstance: () => animationRef.current,
|
||||
play: () => animationRef.current?.play(),
|
||||
pause: () => animationRef.current?.pause(),
|
||||
stop: () => animationRef.current?.stop(),
|
||||
destroy: () => {
|
||||
animationRef.current?.destroy()
|
||||
animationRef.current = null
|
||||
},
|
||||
setSpeed: (nextSpeed) => animationRef.current?.setSpeed(nextSpeed),
|
||||
setDirection: (nextDirection) =>
|
||||
animationRef.current?.setDirection(nextDirection),
|
||||
goToAndPlay: (value, isFrame) =>
|
||||
animationRef.current?.goToAndPlay(value, isFrame),
|
||||
goToAndStop: (value, isFrame) =>
|
||||
animationRef.current?.goToAndStop(value, isFrame),
|
||||
}),
|
||||
[],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef.current
|
||||
|
||||
if (!container || (!animationData && !path)) {
|
||||
return
|
||||
}
|
||||
|
||||
const animation = lottie.loadAnimation({
|
||||
container,
|
||||
renderer,
|
||||
loop: loopRef.current,
|
||||
autoplay: autoplayRef.current,
|
||||
initialSegment,
|
||||
name,
|
||||
assetsPath,
|
||||
rendererSettings: rendererSettings as
|
||||
| SVGRendererConfig
|
||||
| CanvasRendererConfig
|
||||
| HTMLRendererConfig
|
||||
| undefined,
|
||||
...(animationData ? { animationData } : {}),
|
||||
...(path ? { path } : {}),
|
||||
})
|
||||
|
||||
animationRef.current = animation
|
||||
animation.setSpeed(speedRef.current)
|
||||
animation.setDirection(directionRef.current)
|
||||
|
||||
animation.addEventListener('complete', (event) => {
|
||||
completeCallbackRef.current?.(event)
|
||||
})
|
||||
animation.addEventListener('loopComplete', (event) => {
|
||||
loopCompleteCallbackRef.current?.(event)
|
||||
})
|
||||
animation.addEventListener('enterFrame', (event) => {
|
||||
enterFrameCallbackRef.current?.(event)
|
||||
})
|
||||
animation.addEventListener('DOMLoaded', (event) => {
|
||||
domLoadedCallbackRef.current?.(event)
|
||||
})
|
||||
animation.addEventListener('data_ready', (event) => {
|
||||
dataReadyCallbackRef.current?.(event)
|
||||
})
|
||||
animation.addEventListener('data_failed', (event) => {
|
||||
dataFailedCallbackRef.current?.(event)
|
||||
})
|
||||
|
||||
if (segmentsRef.current) {
|
||||
animation.playSegments(segmentsRef.current, forceSegmentsRef.current)
|
||||
}
|
||||
|
||||
if (isPausedRef.current) {
|
||||
animation.pause()
|
||||
}
|
||||
|
||||
hasSyncedPauseStateRef.current = false
|
||||
|
||||
return () => {
|
||||
animation.destroy()
|
||||
animationRef.current = null
|
||||
hasSyncedPauseStateRef.current = false
|
||||
}
|
||||
}, [
|
||||
animationData,
|
||||
path,
|
||||
renderer,
|
||||
initialSegment,
|
||||
name,
|
||||
assetsPath,
|
||||
rendererSettings,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
const animation = animationRef.current
|
||||
|
||||
if (!animation) {
|
||||
return
|
||||
}
|
||||
|
||||
animation.setSpeed(speed)
|
||||
}, [speed])
|
||||
|
||||
useEffect(() => {
|
||||
const animation = animationRef.current
|
||||
|
||||
if (!animation) {
|
||||
return
|
||||
}
|
||||
|
||||
animation.setDirection(direction)
|
||||
}, [direction])
|
||||
|
||||
useEffect(() => {
|
||||
const animation = animationRef.current
|
||||
|
||||
if (!animation) {
|
||||
return
|
||||
}
|
||||
|
||||
if (typeof loop === 'boolean') {
|
||||
animation.setLoop(loop)
|
||||
return
|
||||
}
|
||||
|
||||
animation.loop = loop
|
||||
}, [loop])
|
||||
|
||||
useEffect(() => {
|
||||
const animation = animationRef.current
|
||||
|
||||
if (!animation || !segments) {
|
||||
return
|
||||
}
|
||||
|
||||
animation.playSegments(segments, forceSegments)
|
||||
}, [segments, forceSegments])
|
||||
|
||||
useEffect(() => {
|
||||
const animation = animationRef.current
|
||||
|
||||
if (!animation) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!hasSyncedPauseStateRef.current) {
|
||||
hasSyncedPauseStateRef.current = true
|
||||
return
|
||||
}
|
||||
|
||||
if (isPaused) {
|
||||
animation.pause()
|
||||
return
|
||||
}
|
||||
|
||||
animation.play()
|
||||
}, [isPaused])
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={cn('relative h-full w-full overflow-hidden', className)}
|
||||
{...divProps}
|
||||
/>
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -20,6 +20,7 @@ export interface SmartImageProps
|
||||
imgClassName?: string
|
||||
skeletonClassName?: string
|
||||
fallbackClassName?: string
|
||||
|
||||
fallback?: ReactNode
|
||||
fallbackSrc?: string
|
||||
placeholderSrc?: string
|
||||
|
||||
@@ -63,7 +63,7 @@ export function DesktopControl() {
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
'flex h-design-100 w-full items-center gap-design-16 overflow-hidden'
|
||||
'flex h-design-100 w-full items-center gap-design-12 overflow-hidden'
|
||||
}
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -3,6 +3,9 @@ import { DesktopAnimal } from '@/features/game/components/desktop/desktop-animal
|
||||
import { DesktopControl } from '@/features/game/components/desktop/desktop-control.tsx'
|
||||
import { DesktopGameHistory } from '@/features/game/components/desktop/desktop-game-history.tsx'
|
||||
import { DesktopStatusLine } from '@/features/game/components/desktop/desktop-status.tsx'
|
||||
import DesktopLoginModal from '@/features/game/modal/desktop/desktop-login-modal.tsx'
|
||||
import DesktopRegisterModal from '@/features/game/modal/desktop/desktop-register-modal.tsx'
|
||||
import DesktopUserInfoModal from '@/features/game/modal/desktop/desktop-userInfo-modal.tsx'
|
||||
|
||||
export function PcEntry() {
|
||||
return (
|
||||
@@ -36,6 +39,12 @@ export function PcEntry() {
|
||||
>
|
||||
<DesktopControl />
|
||||
</div>
|
||||
{/*登录弹窗*/}
|
||||
<DesktopLoginModal />
|
||||
{/*注册弹窗 */}
|
||||
{/*<DesktopRegisterModal />*/}
|
||||
{/* 用户信息弹窗 */}
|
||||
<DesktopUserInfoModal />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
101
src/features/game/modal/desktop/desktop-login-modal.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import { motion } from 'motion/react'
|
||||
import { useState } from 'react'
|
||||
import loginBg from '@/assets/system/login-bg.webp'
|
||||
import rightImg from '@/assets/system/right.webp'
|
||||
import { CenterModal } from '@/components/center-modal.tsx'
|
||||
import { SmartImage } from '@/components/smart-image.tsx'
|
||||
|
||||
function DesktopLoginModal() {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
function handleSubmit() {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<CenterModal
|
||||
open={open}
|
||||
onClose={() => {}}
|
||||
title={<div className={'modal-title-glow'}>登录</div>}
|
||||
titleAlign="center"
|
||||
className={'w-design-980 h-design-540'}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
'flex flex-col items-center justify-between gap-design-20 px-design-20'
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
'h-design-375 flex flex-col gap-design-45 w-full bg-[#060B0F]/50 p-design-50'
|
||||
}
|
||||
>
|
||||
<div className={'flex items-center'}>
|
||||
<div
|
||||
className={
|
||||
'w-design-160 shrink-0 text-left text-design-24 text-[#58ADAF]'
|
||||
}
|
||||
>
|
||||
Akun/TEL:
|
||||
</div>
|
||||
<input
|
||||
className={'flex-1 text-left'}
|
||||
placeholder={'Silakan masukkan akun atau nomor ponsel Anda.'}
|
||||
/>
|
||||
</div>
|
||||
<div className={'flex items-center'}>
|
||||
<div
|
||||
className={
|
||||
'w-design-160 shrink-0 text-left text-design-24 text-[#58ADAF]'
|
||||
}
|
||||
>
|
||||
Kata Sandi:
|
||||
</div>
|
||||
<input
|
||||
className={'flex-1 text-left'}
|
||||
placeholder={'Masukkan Kata Sandi'}
|
||||
/>
|
||||
</div>
|
||||
<div className={'flex items-center justify-around'}>
|
||||
<div className={'flex items-center gap-design-10'}>
|
||||
<div
|
||||
className={
|
||||
'flex items-center justify-center bg-[#040B0F] border border-[#549195] rounded-md w-design-38 h-design-38'
|
||||
}
|
||||
>
|
||||
<SmartImage alt={'right'} src={rightImg} />
|
||||
</div>
|
||||
<div className={'text-[#549195]'}>Daftar Akun</div>
|
||||
</div>
|
||||
<div className={'flex items-center gap-design-10'}>
|
||||
<div
|
||||
className={
|
||||
'flex items-center justify-center bg-[#040B0F] border border-[#549195] rounded-md w-design-38 h-design-38'
|
||||
}
|
||||
>
|
||||
<SmartImage alt={'right'} src={rightImg} />
|
||||
</div>
|
||||
<div className={'text-[#549195]'}>Ingat Kata Sandi</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
onClick={handleSubmit}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
style={{
|
||||
backgroundImage: `url(${loginBg})`,
|
||||
backgroundSize: '100% 100%',
|
||||
}}
|
||||
className={
|
||||
'w-design-390 h-design-110 flex items-center justify-center text-design-32 modal-title-glow font-bold cursor-pointer'
|
||||
}
|
||||
>
|
||||
MASUK
|
||||
</motion.div>
|
||||
</div>
|
||||
</CenterModal>
|
||||
)
|
||||
}
|
||||
|
||||
export default DesktopLoginModal
|
||||
125
src/features/game/modal/desktop/desktop-register-modal.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
import { motion } from 'motion/react'
|
||||
import { useState } from 'react'
|
||||
import loginBg from '@/assets/system/login-bg.webp'
|
||||
import rightImg from '@/assets/system/right.webp'
|
||||
import { CenterModal } from '@/components/center-modal.tsx'
|
||||
import { SmartImage } from '@/components/smart-image.tsx'
|
||||
|
||||
function DesktopRegisterModal() {
|
||||
const [open, setOpen] = useState(true)
|
||||
|
||||
function handleSubmit() {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<CenterModal
|
||||
open={open}
|
||||
onClose={() => {}}
|
||||
title={<div className={'modal-title-glow'}>注册</div>}
|
||||
titleAlign="center"
|
||||
className={'w-design-980 h-design-740'}
|
||||
>
|
||||
<div
|
||||
className={'flex flex-col items-center justify-between px-design-20'}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
'h-design-490 flex flex-col gap-design-30 w-full bg-[#060B0F]/50 p-design-50'
|
||||
}
|
||||
>
|
||||
<div className={'flex items-center'}>
|
||||
<div
|
||||
className={
|
||||
'w-design-160 shrink-0 text-left text-design-24 text-[#58ADAF]'
|
||||
}
|
||||
>
|
||||
Akun/TEL:
|
||||
</div>
|
||||
<input
|
||||
className={'flex-1 text-left'}
|
||||
placeholder={'Silakan masukkan akun atau nomor ponsel Anda.'}
|
||||
/>
|
||||
</div>
|
||||
<div className={'flex items-center'}>
|
||||
<div
|
||||
className={
|
||||
'w-design-160 shrink-0 text-left text-design-24 text-[#58ADAF]'
|
||||
}
|
||||
>
|
||||
Kata Sandi:
|
||||
</div>
|
||||
<input
|
||||
className={'flex-1 text-left'}
|
||||
placeholder={'Masukkan Kata Sandi'}
|
||||
/>
|
||||
</div>
|
||||
<div className={'flex items-center'}>
|
||||
<div
|
||||
className={
|
||||
'w-design-160 shrink-0 text-left text-design-24 text-[#58ADAF]'
|
||||
}
|
||||
>
|
||||
Kata Sandi:
|
||||
</div>
|
||||
<input
|
||||
className={'flex-1 text-left'}
|
||||
placeholder={'Masukkan Kata Sandi'}
|
||||
/>
|
||||
</div>
|
||||
<div className={'flex items-center'}>
|
||||
<div
|
||||
className={
|
||||
'w-design-160 shrink-0 text-left text-design-24 text-[#58ADAF]'
|
||||
}
|
||||
>
|
||||
Kata Sandi:
|
||||
</div>
|
||||
<input
|
||||
className={'flex-1 text-left'}
|
||||
placeholder={'Masukkan Kata Sandi'}
|
||||
/>
|
||||
</div>
|
||||
<div className={'flex items-center justify-around'}>
|
||||
<div className={'flex items-center gap-design-10'}>
|
||||
<div
|
||||
className={
|
||||
'flex items-center justify-center bg-[#040B0F] border border-[#549195] rounded-md w-design-38 h-design-38'
|
||||
}
|
||||
>
|
||||
<SmartImage alt={'right'} src={rightImg} />
|
||||
</div>
|
||||
<div className={'text-[#549195]'}>Daftar Akun</div>
|
||||
</div>
|
||||
<div className={'flex items-center gap-design-10'}>
|
||||
<div
|
||||
className={
|
||||
'flex items-center justify-center bg-[#040B0F] border border-[#549195] rounded-md w-design-38 h-design-38'
|
||||
}
|
||||
>
|
||||
<SmartImage alt={'right'} src={rightImg} />
|
||||
</div>
|
||||
<div className={'text-[#549195]'}>Ingat Kata Sandi</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
onClick={handleSubmit}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
style={{
|
||||
backgroundImage: `url(${loginBg})`,
|
||||
backgroundSize: '100% 100%',
|
||||
}}
|
||||
className={
|
||||
'w-design-390 h-design-110 flex items-center justify-center text-design-32 modal-title-glow font-bold cursor-pointer'
|
||||
}
|
||||
>
|
||||
MASUK
|
||||
</motion.div>
|
||||
</div>
|
||||
</CenterModal>
|
||||
)
|
||||
}
|
||||
|
||||
export default DesktopRegisterModal
|
||||
136
src/features/game/modal/desktop/desktop-userInfo-modal.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
import { CircleUserRound, Mail } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import userInfoBg from '@/assets/system/userInfo-bg.webp'
|
||||
import { CenterModal } from '@/components/center-modal.tsx'
|
||||
import { cn } from '@/lib/untils'
|
||||
|
||||
type UserInfoTabKey = 'profile' | 'message'
|
||||
|
||||
const USER_INFO_TABS: Array<{
|
||||
key: UserInfoTabKey
|
||||
label: string
|
||||
icon: typeof CircleUserRound
|
||||
}> = [
|
||||
{
|
||||
key: 'profile',
|
||||
label: '个人信息',
|
||||
icon: CircleUserRound,
|
||||
},
|
||||
{
|
||||
key: 'message',
|
||||
label: '站内消息',
|
||||
icon: Mail,
|
||||
},
|
||||
]
|
||||
|
||||
function DesktopUserInfoModal() {
|
||||
const [open, setOpen] = useState(true)
|
||||
const [activeTab, setActiveTab] = useState<UserInfoTabKey>('profile')
|
||||
|
||||
function handleSubmit() {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<CenterModal
|
||||
open={open}
|
||||
onClose={handleSubmit}
|
||||
title={
|
||||
<div className={'modal-title-glow text-design-26'}>Biomond Balance</div>
|
||||
}
|
||||
isNormalBg={true}
|
||||
titleAlign="left"
|
||||
className={'w-design-980 h-design-590'}
|
||||
>
|
||||
<div className={'relative flex h-[96%] w-full'}>
|
||||
<div className={'relative w-design-230 shrink-0'}>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="absolute right-0 top-0 h-full w-[calc(var(--design-unit)*2)] bg-[linear-gradient(180deg,rgba(68,244,255,0)_0%,rgba(68,244,255,0.55)_12%,rgba(130,255,255,0.95)_50%,rgba(68,244,255,0.55)_88%,rgba(68,244,255,0)_100%)] shadow-[0_0_calc(var(--design-unit)*8)_rgba(49,208,255,0.45)]"
|
||||
/>
|
||||
|
||||
{USER_INFO_TABS.map((tab) => {
|
||||
const Icon = tab.icon
|
||||
const isActive = tab.key === activeTab
|
||||
|
||||
return (
|
||||
<button
|
||||
key={tab.key}
|
||||
type="button"
|
||||
onClick={() => setActiveTab(tab.key)}
|
||||
className={cn(
|
||||
'relative flex h-design-150 w-full flex-col items-center justify-center gap-design-10 overflow-hidden transition',
|
||||
isActive
|
||||
? 'text-[#FEEEB0]'
|
||||
: 'text-[#58ADAF] hover:text-[#BFEAEC]',
|
||||
)}
|
||||
>
|
||||
{isActive ? (
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="absolute inset-y-0 right-0 w-full bg-[linear-gradient(to_left,rgba(254,238,176,0.46)_0%,rgba(254,238,176,0.28)_42%,rgba(254,238,176,0.12)_68%,rgba(254,238,176,0)_100%)]"
|
||||
/>
|
||||
) : null}
|
||||
{isActive ? (
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="absolute right-0 top-1/2 h-[72%] w-[calc(var(--design-unit)*3)] -translate-y-1/2 rounded-l-full bg-[linear-gradient(180deg,rgba(255,248,214,0.96)_0%,rgba(254,238,176,0.92)_48%,rgba(232,188,112,0.88)_100%)] shadow-[-2px_0_calc(var(--design-unit)*8)_rgba(254,238,176,0.36)]"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Icon
|
||||
size={60}
|
||||
className={cn(
|
||||
'relative z-10 transition',
|
||||
isActive &&
|
||||
'drop-shadow-[0_0_calc(var(--design-unit)*8)_rgba(254,238,176,0.5)]',
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
'relative z-10 text-design-24',
|
||||
isActive && 'modal-title-gold-glow',
|
||||
)}
|
||||
>
|
||||
{tab.label}
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className={'flex-1'}>
|
||||
<div
|
||||
className={
|
||||
'flex h-full w-full items-start justify-start bg-top bg-no-repeat px-design-40 py-design-32'
|
||||
}
|
||||
style={{
|
||||
backgroundImage: `url(${userInfoBg})`,
|
||||
backgroundSize: '120% 100%',
|
||||
}}
|
||||
>
|
||||
{activeTab === 'profile' ? (
|
||||
<div className="space-y-3 text-design-22 text-[#D7FAFF]">
|
||||
<div className="modal-title-gold-glow text-design-28">
|
||||
个人信息
|
||||
</div>
|
||||
<div>UID: 10009231</div>
|
||||
<div>账户等级: VIP 3</div>
|
||||
<div>绑定手机: 138****8899</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3 text-design-22 text-[#D7FAFF]">
|
||||
<div className="modal-title-gold-glow text-design-28">
|
||||
站内消息
|
||||
</div>
|
||||
<div>您当前没有新的站内消息。</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CenterModal>
|
||||
)
|
||||
}
|
||||
|
||||
export default DesktopUserInfoModal
|
||||
@@ -42,10 +42,6 @@
|
||||
min-width: calc(var(--design-unit) * --value(integer));
|
||||
}
|
||||
|
||||
@utility max-w-design-* {
|
||||
max-width: calc(var(--design-unit) * --value(integer));
|
||||
}
|
||||
|
||||
@utility text-design-* {
|
||||
font-size: calc(var(--design-unit) * --value(integer));
|
||||
}
|
||||
@@ -54,8 +50,13 @@
|
||||
gap: calc(var(--design-unit) * --value(integer));
|
||||
}
|
||||
|
||||
@utility pt-design-* {
|
||||
padding-top: calc(var(--design-unit) * --value(integer));
|
||||
}
|
||||
|
||||
@utility px-design-* {
|
||||
padding-inline: calc(var(--design-unit) * --value(integer));
|
||||
padding-left: calc(var(--design-unit) * --value(integer));
|
||||
padding-right: calc(var(--design-unit) * --value(integer));
|
||||
}
|
||||
|
||||
@utility p-design-* {
|
||||
@@ -108,6 +109,10 @@
|
||||
top: calc(var(--design-unit) * --value(integer));
|
||||
}
|
||||
|
||||
@utility right-design-* {
|
||||
right: calc(var(--design-unit) * --value(integer));
|
||||
}
|
||||
|
||||
@layer base {
|
||||
html {
|
||||
@apply h-full w-full text-design-16;
|
||||
@@ -132,6 +137,25 @@
|
||||
linear-gradient(180deg, #07111f 0%, #040812 100%);
|
||||
color: #f8fafc;
|
||||
}
|
||||
|
||||
input {
|
||||
@apply border border-transparent bg-[#135E65] text-[#D9FFFF] py-design-15 px-design-30 text-design-20 rounded-md outline-none transition;
|
||||
}
|
||||
|
||||
input::placeholder {
|
||||
color: rgba(116, 173, 175, 0.72);
|
||||
}
|
||||
|
||||
input:focus,
|
||||
input:focus-visible {
|
||||
border-color: rgba(110, 255, 255, 0.72);
|
||||
outline: none;
|
||||
box-shadow:
|
||||
0 0 0 calc(var(--design-unit) * 1.5) rgba(110, 255, 255, 0.16),
|
||||
0 0 calc(var(--design-unit) * 8) rgba(48, 214, 255, 0.36),
|
||||
0 0 calc(var(--design-unit) * 18) rgba(18, 162, 255, 0.22),
|
||||
inset 0 0 calc(var(--design-unit) * 6) rgba(110, 255, 255, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
@@ -198,6 +222,28 @@
|
||||
drop-shadow(0 0 calc(var(--design-unit) * 14) rgba(49, 208, 255, 0.28));
|
||||
}
|
||||
|
||||
.modal-close-glow {
|
||||
filter: drop-shadow(
|
||||
0 0 calc(var(--design-unit) * 4) rgba(110, 255, 255, 0.95)
|
||||
)
|
||||
drop-shadow(0 0 calc(var(--design-unit) * 12) rgba(48, 214, 255, 0.78))
|
||||
drop-shadow(0 0 calc(var(--design-unit) * 24) rgba(18, 162, 255, 0.5));
|
||||
}
|
||||
|
||||
.modal-title-glow {
|
||||
text-shadow:
|
||||
0 0 calc(var(--design-unit) * 2) rgba(224, 255, 255, 0.95),
|
||||
0 0 calc(var(--design-unit) * 6) rgba(108, 247, 255, 0.82),
|
||||
0 0 calc(var(--design-unit) * 14) rgba(48, 214, 255, 0.58);
|
||||
}
|
||||
|
||||
.modal-title-gold-glow {
|
||||
text-shadow:
|
||||
0 0 calc(var(--design-unit) * 2) rgba(255, 248, 235, 0.9),
|
||||
0 0 calc(var(--design-unit) * 8) rgba(254, 238, 176, 0.64),
|
||||
0 0 calc(var(--design-unit) * 16) rgba(254, 238, 176, 0.38);
|
||||
}
|
||||
|
||||
.desktop-control-chip {
|
||||
margin-left: calc(var(--design-unit) * -4);
|
||||
margin-right: calc(var(--design-unit) * -18);
|
||||
@@ -209,7 +255,7 @@
|
||||
}
|
||||
|
||||
.desktop-control-actions {
|
||||
margin-left: calc(var(--design-unit) * -18);
|
||||
margin-left: calc(var(--design-unit) * -16);
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,11 @@ export default defineConfig({
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 9999,
|
||||
host: '0.0.0.0',
|
||||
allowedHosts: ['darlena-nonexpiring-cathie.ngrok-free.dev'],
|
||||
},
|
||||
plugins: [
|
||||
tanstackRouter({
|
||||
target: 'react',
|
||||
|
||||