feat(modal): 添加居中模态框组件及游戏相关弹窗功能
- 新增 CenterModal 组件,支持标题、关闭按钮、背景图片等功能 - 添加键盘 ESC 键关闭模态框和页面滚动锁定功能 - 创建桌面端登录、注册和用户信息三个业务模态框 - 集成 lottie-web 动画库并创建 LottiePlayer 组件 - 在样式文件中添加模态框相关的 CSS 类和输入框样式 - 更新桌面入口文件集成登录和用户信息模态框 - 调整桌面控制组件间距和样式配置
@@ -32,6 +32,7 @@
|
|||||||
"dayjs": "^1.11.20",
|
"dayjs": "^1.11.20",
|
||||||
"i18next": "^26.0.5",
|
"i18next": "^26.0.5",
|
||||||
"ky": "^2.0.1",
|
"ky": "^2.0.1",
|
||||||
|
"lottie-web": "^5.13.0",
|
||||||
"lucide-react": "^1.9.0",
|
"lucide-react": "^1.9.0",
|
||||||
"motion": "^12.38.0",
|
"motion": "^12.38.0",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
|
|||||||
8
pnpm-lock.yaml
generated
@@ -29,6 +29,9 @@ importers:
|
|||||||
ky:
|
ky:
|
||||||
specifier: ^2.0.1
|
specifier: ^2.0.1
|
||||||
version: 2.0.1
|
version: 2.0.1
|
||||||
|
lottie-web:
|
||||||
|
specifier: ^5.13.0
|
||||||
|
version: 5.13.0
|
||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: ^1.9.0
|
specifier: ^1.9.0
|
||||||
version: 1.9.0(react@19.2.5)
|
version: 1.9.0(react@19.2.5)
|
||||||
@@ -1565,6 +1568,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-Ajzxb8CM6WAnFjgiloPsI3bF+WCxcvhdIG3KNA2KN962+tdBsHcuQ4k4qX/EcS/2CRkcc0iAkR956Nib6aXU/Q==}
|
resolution: {integrity: sha512-Ajzxb8CM6WAnFjgiloPsI3bF+WCxcvhdIG3KNA2KN962+tdBsHcuQ4k4qX/EcS/2CRkcc0iAkR956Nib6aXU/Q==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
lottie-web@5.13.0:
|
||||||
|
resolution: {integrity: sha512-+gfBXl6sxXMPe8tKQm7qzLnUy5DUPJPKIyRHwtpCpyUEYjHYRJC/5gjUvdkuO2c3JllrPtHXH5UJJK8LRYl5yQ==}
|
||||||
|
|
||||||
lru-cache@5.1.1:
|
lru-cache@5.1.1:
|
||||||
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
||||||
|
|
||||||
@@ -3387,6 +3393,8 @@ snapshots:
|
|||||||
|
|
||||||
longest@2.0.1: {}
|
longest@2.0.1: {}
|
||||||
|
|
||||||
|
lottie-web@5.13.0: {}
|
||||||
|
|
||||||
lru-cache@5.1.1:
|
lru-cache@5.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
yallist: 3.1.1
|
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
|
imgClassName?: string
|
||||||
skeletonClassName?: string
|
skeletonClassName?: string
|
||||||
fallbackClassName?: string
|
fallbackClassName?: string
|
||||||
|
|
||||||
fallback?: ReactNode
|
fallback?: ReactNode
|
||||||
fallbackSrc?: string
|
fallbackSrc?: string
|
||||||
placeholderSrc?: string
|
placeholderSrc?: string
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export function DesktopControl() {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={
|
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
|
<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 { DesktopControl } from '@/features/game/components/desktop/desktop-control.tsx'
|
||||||
import { DesktopGameHistory } from '@/features/game/components/desktop/desktop-game-history.tsx'
|
import { DesktopGameHistory } from '@/features/game/components/desktop/desktop-game-history.tsx'
|
||||||
import { DesktopStatusLine } from '@/features/game/components/desktop/desktop-status.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() {
|
export function PcEntry() {
|
||||||
return (
|
return (
|
||||||
@@ -36,6 +39,12 @@ export function PcEntry() {
|
|||||||
>
|
>
|
||||||
<DesktopControl />
|
<DesktopControl />
|
||||||
</div>
|
</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));
|
min-width: calc(var(--design-unit) * --value(integer));
|
||||||
}
|
}
|
||||||
|
|
||||||
@utility max-w-design-* {
|
|
||||||
max-width: calc(var(--design-unit) * --value(integer));
|
|
||||||
}
|
|
||||||
|
|
||||||
@utility text-design-* {
|
@utility text-design-* {
|
||||||
font-size: calc(var(--design-unit) * --value(integer));
|
font-size: calc(var(--design-unit) * --value(integer));
|
||||||
}
|
}
|
||||||
@@ -54,8 +50,13 @@
|
|||||||
gap: calc(var(--design-unit) * --value(integer));
|
gap: calc(var(--design-unit) * --value(integer));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@utility pt-design-* {
|
||||||
|
padding-top: calc(var(--design-unit) * --value(integer));
|
||||||
|
}
|
||||||
|
|
||||||
@utility px-design-* {
|
@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-* {
|
@utility p-design-* {
|
||||||
@@ -108,6 +109,10 @@
|
|||||||
top: calc(var(--design-unit) * --value(integer));
|
top: calc(var(--design-unit) * --value(integer));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@utility right-design-* {
|
||||||
|
right: calc(var(--design-unit) * --value(integer));
|
||||||
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
html {
|
html {
|
||||||
@apply h-full w-full text-design-16;
|
@apply h-full w-full text-design-16;
|
||||||
@@ -132,6 +137,25 @@
|
|||||||
linear-gradient(180deg, #07111f 0%, #040812 100%);
|
linear-gradient(180deg, #07111f 0%, #040812 100%);
|
||||||
color: #f8fafc;
|
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 {
|
@layer utilities {
|
||||||
@@ -198,6 +222,28 @@
|
|||||||
drop-shadow(0 0 calc(var(--design-unit) * 14) rgba(49, 208, 255, 0.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 {
|
.desktop-control-chip {
|
||||||
margin-left: calc(var(--design-unit) * -4);
|
margin-left: calc(var(--design-unit) * -4);
|
||||||
margin-right: calc(var(--design-unit) * -18);
|
margin-right: calc(var(--design-unit) * -18);
|
||||||
@@ -209,7 +255,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.desktop-control-actions {
|
.desktop-control-actions {
|
||||||
margin-left: calc(var(--design-unit) * -18);
|
margin-left: calc(var(--design-unit) * -16);
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ export default defineConfig({
|
|||||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
server: {
|
||||||
|
port: 9999,
|
||||||
|
host: '0.0.0.0',
|
||||||
|
allowedHosts: ['darlena-nonexpiring-cathie.ngrok-free.dev'],
|
||||||
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
tanstackRouter({
|
tanstackRouter({
|
||||||
target: 'react',
|
target: 'react',
|
||||||
|
|||||||