refactor(game): 重构项目结构,优化链路, 移动端适配
- 移除 useGameBoardVm 数据层实施说明文档 - 移除核心玩法与前端规则摘要文档 - 移除游戏模块数据与界面分层第一阶段实施稿文档 - 清理与数据层重构相关的技术方案说明 - 删除关于 PC 和 Mobile 界面分离的设计规划 - 移除 view-model hooks 架构设计相关内容
This commit is contained in:
170
src/components/mobile-center-modal.tsx
Normal file
170
src/components/mobile-center-modal.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
import { type ReactNode, useEffect, useRef } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import mobileModalHeader from '@/assets/system/mobile-modal-header.webp'
|
||||
import modalClose from '@/assets/system/modal-close.webp'
|
||||
import { acquireBodyScrollLock } from '@/lib/dom/body-scroll-lock'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface MobileCenterModalProps {
|
||||
open: boolean
|
||||
onClose?: () => void
|
||||
title?: ReactNode
|
||||
titleAlign?: 'left' | 'center'
|
||||
isShowClose?: boolean
|
||||
isNormalBg?: boolean
|
||||
children?: ReactNode
|
||||
className?: string
|
||||
backdropClassName?: string
|
||||
}
|
||||
|
||||
const MOBILE_MODAL_HEADER_HEIGHT = 'calc(var(--design-unit)*82)'
|
||||
const MOBILE_MODAL_CONTENT_TOP = 'calc(var(--design-unit)*40)'
|
||||
|
||||
export function MobileCenterModal({
|
||||
open,
|
||||
onClose,
|
||||
title,
|
||||
titleAlign = 'left',
|
||||
isShowClose = true,
|
||||
children,
|
||||
className,
|
||||
backdropClassName,
|
||||
}: MobileCenterModalProps) {
|
||||
const { t } = useTranslation()
|
||||
const onCloseRef = useRef(onClose)
|
||||
const handleClose = () => {
|
||||
onClose?.()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
onCloseRef.current = onClose
|
||||
}, [onClose])
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || typeof document === 'undefined') {
|
||||
return
|
||||
}
|
||||
|
||||
const releaseBodyScrollLock = acquireBodyScrollLock()
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
onCloseRef.current?.()
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown)
|
||||
|
||||
return () => {
|
||||
releaseBodyScrollLock()
|
||||
window.removeEventListener('keydown', handleKeyDown)
|
||||
}
|
||||
}, [open])
|
||||
|
||||
if (!open || typeof document === 'undefined') {
|
||||
return null
|
||||
}
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
className={cn(
|
||||
'fixed inset-0 z-50 flex items-center justify-center bg-slate-950/80 px-design-12 py-design-14',
|
||||
backdropClassName,
|
||||
)}
|
||||
>
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={
|
||||
typeof title === 'string'
|
||||
? title
|
||||
: t('commonUi.modal.defaultAriaLabel')
|
||||
}
|
||||
className={cn(
|
||||
'relative flex min-h-0 w-full flex-col overflow-visible rounded-[calc(var(--design-unit)*12)] text-white shadow-[0_0_calc(var(--design-unit)*22)_rgba(21,213,232,0.18)]',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'relative h-full min-h-0 w-full overflow-visible rounded-b-[calc(var(--design-unit)*10)] bg-[linear-gradient(180deg,rgba(5,37,47,0.98)_0%,rgba(2,21,31,0.98)_58%,rgba(1,12,20,0.99)_100%)] shadow-[inset_0_0_calc(var(--design-unit)*24)_rgba(30,206,222,0.18)]',
|
||||
title ? '' : 'pt-design-40',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute inset-x-0 bottom-0 z-[1] rounded-b-[calc(var(--design-unit)*10)] border-x border-b border-[rgba(108,205,207,0.72)]"
|
||||
style={{
|
||||
top: MOBILE_MODAL_CONTENT_TOP,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute z-20 shrink-0"
|
||||
style={{
|
||||
top: 'calc(var(--design-unit)*-2)',
|
||||
left: 'calc(var(--design-unit)*-3)',
|
||||
right: 'calc(var(--design-unit)*-3)',
|
||||
height: 'calc(var(--design-unit)*86)',
|
||||
backgroundImage: `url(${mobileModalHeader})`,
|
||||
backgroundPosition: 'top center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundSize: '100% 100%',
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="pointer-events-none absolute top-0 z-30 shrink-0 px-design-22"
|
||||
style={{
|
||||
left: 'calc(var(--design-unit)*-8)',
|
||||
right: 'calc(var(--design-unit)*-8)',
|
||||
height: MOBILE_MODAL_HEADER_HEIGHT,
|
||||
}}
|
||||
>
|
||||
{title ? (
|
||||
<div
|
||||
className={cn(
|
||||
'pointer-events-auto flex h-design-44 w-full items-center text-design-22 font-semibold tracking-[0.05em] text-cyan-50',
|
||||
titleAlign === 'center'
|
||||
? 'justify-center text-center'
|
||||
: 'justify-start text-left',
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{isShowClose && onClose ? (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('commonUi.modal.close')}
|
||||
onClick={handleClose}
|
||||
className="pointer-events-auto absolute top-design-11 right-design-15 inline-flex h-design-24 w-design-34 cursor-pointer items-center justify-center rounded-full transition hover:scale-105 active:scale-95"
|
||||
>
|
||||
<img
|
||||
src={modalClose}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className="modal-close-glow relative z-10 h-design-32 w-design-26 object-contain"
|
||||
/>
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
<div
|
||||
className="relative z-10 min-h-0 w-full overflow-hidden"
|
||||
style={{
|
||||
height: `calc(100% - ${MOBILE_MODAL_CONTENT_TOP})`,
|
||||
marginTop: MOBILE_MODAL_CONTENT_TOP,
|
||||
}}
|
||||
>
|
||||
<div className="h-full min-h-0 w-full overflow-y-auto overscroll-contain">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user