Files
36-character-flower/src/modal/mobile/mobile-period-history-drawer.tsx
JiaJun bfb4b76611 refactor(game): 重构项目结构,优化链路, 移动端适配
- 移除 useGameBoardVm 数据层实施说明文档
- 移除核心玩法与前端规则摘要文档
- 移除游戏模块数据与界面分层第一阶段实施稿文档
- 清理与数据层重构相关的技术方案说明
- 删除关于 PC 和 Mobile 界面分离的设计规划
- 移除 view-model hooks 架构设计相关内容
2026-06-03 17:21:13 +08:00

182 lines
6.4 KiB
TypeScript

import { X } from 'lucide-react'
import { AnimatePresence, motion, useReducedMotion } from 'motion/react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { PeriodHistoryList } from '@/features/game/components/shared/period-history-list'
import {
DEFAULT_PERIOD_HISTORY_LIMIT,
type PeriodHistoryDisplayItem,
usePeriodHistoryVm,
} from '@/hooks/use-period-history-vm'
import { useModalStore } from '@/store'
const OVERLAY_EASE = [0.16, 1, 0.3, 1] as const
const DRAWER_TRANSITION = {
type: 'tween',
duration: 0.34,
ease: OVERLAY_EASE,
} as const
interface PeriodHistoryDrawerLabels {
close: string
empty: string
failed: string
loading: string
retry: string
title: string
}
interface MobilePeriodHistoryDrawerViewProps {
isError: boolean
isLoading: boolean
items: PeriodHistoryDisplayItem[]
labels: PeriodHistoryDrawerLabels
onClose: () => void
onRetry: () => void
open: boolean
}
export function MobilePeriodHistoryDrawer() {
const { t } = useTranslation()
const open = useModalStore((state) => state.modals.desktopPeriodHistory)
const setModalOpen = useModalStore((state) => state.setModalOpen)
const vm = usePeriodHistoryVm({
enabled: open,
limit: DEFAULT_PERIOD_HISTORY_LIMIT,
})
const handleClose = () => {
setModalOpen('desktopPeriodHistory', false)
}
return (
<MobilePeriodHistoryDrawerView
open={open}
items={vm.items}
isLoading={vm.isLoading}
isError={vm.isError}
labels={{
close: t('gameDesktop.periodHistory.close'),
empty: t('gameDesktop.periodHistory.empty'),
failed: t('gameDesktop.periodHistory.failed'),
loading: t('gameDesktop.periodHistory.loading'),
retry: t('gameDesktop.periodHistory.retry'),
title: t('gameDesktop.periodHistory.title'),
}}
onClose={handleClose}
onRetry={() => void vm.refetch()}
/>
)
}
export function MobilePeriodHistoryDrawerView({
isError,
isLoading,
items,
labels,
onClose,
onRetry,
open,
}: MobilePeriodHistoryDrawerViewProps) {
const prefersReducedMotion = useReducedMotion()
const [isDrawerAnimating, setIsDrawerAnimating] = useState(false)
return (
<AnimatePresence>
{open && (
<>
<motion.button
type="button"
aria-label={labels.close}
className="fixed left-0 right-0 top-0 bottom-[calc(var(--design-unit)*150)] z-30 cursor-default bg-black/48"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{
duration: prefersReducedMotion ? 0.12 : 0.26,
ease: OVERLAY_EASE,
}}
onClick={onClose}
/>
<motion.aside
role="dialog"
aria-modal="true"
aria-label={labels.title}
className="fixed left-0 top-design-16 bottom-[calc(var(--design-unit)*150)] z-40 flex w-design-1120 max-w-[calc(100vw-var(--design-unit)*24)] origin-left flex-col overflow-hidden rounded-r-[calc(var(--design-unit)*10)] border border-[rgba(81,230,255,0.62)] bg-[linear-gradient(180deg,rgba(6,19,32,0.98),rgba(3,12,22,0.96))] text-[#D5FBFF] shadow-[0_0_calc(var(--design-unit)*18)_rgba(39,216,255,0.28),0_0_calc(var(--design-unit)*54)_rgba(39,216,255,0.16),inset_0_0_calc(var(--design-unit)*18)_rgba(74,224,255,0.16)]"
initial={
prefersReducedMotion
? { opacity: 0 }
: { x: '-100%', opacity: 0.98 }
}
animate={
prefersReducedMotion ? { opacity: 1 } : { x: 0, opacity: 1 }
}
exit={
prefersReducedMotion
? { opacity: 0 }
: { x: '-100%', opacity: 0.98 }
}
transition={
prefersReducedMotion ? { duration: 0.12 } : DRAWER_TRANSITION
}
onAnimationStart={() => setIsDrawerAnimating(true)}
onAnimationComplete={() => setIsDrawerAnimating(false)}
style={
isDrawerAnimating
? { willChange: 'transform, opacity' }
: undefined
}
>
<span
aria-hidden="true"
className="pointer-events-none absolute inset-x-design-8 top-0 h-px bg-[linear-gradient(90deg,transparent,rgba(80,241,255,0.96),transparent)]"
/>
<span
aria-hidden="true"
className="pointer-events-none absolute bottom-0 left-0 h-design-28 w-design-28 border-b-2 border-l-2 border-[#28E6FF]"
/>
<span
aria-hidden="true"
className="pointer-events-none absolute bottom-0 right-0 h-design-28 w-design-28 border-b-2 border-r-2 border-[#28E6FF]"
/>
<div className="relative flex h-design-78 shrink-0 items-center justify-between border-b border-[rgba(80,224,255,0.38)] px-design-42">
<h2 className="text-design-28 font-bold leading-none text-white [text-shadow:0_0_calc(var(--design-unit)*10)_rgba(156,244,255,0.42)]">
{labels.title}
</h2>
<button
type="button"
aria-label={labels.close}
className="flex h-design-42 w-design-42 cursor-pointer items-center justify-center text-[#C8F7FF] transition-colors duration-200 hover:text-white focus-visible:ring-2 focus-visible:ring-[#4FEAFF]"
onClick={onClose}
>
<X size={32} strokeWidth={2.1} />
</button>
</div>
<motion.div
className="history-scroll-hidden min-h-0 flex-1 overflow-y-auto px-design-34 py-design-26"
initial={
prefersReducedMotion ? { opacity: 0 } : { opacity: 0, y: 8 }
}
animate={
prefersReducedMotion ? { opacity: 1 } : { opacity: 1, y: 0 }
}
transition={
prefersReducedMotion
? { duration: 0.12 }
: { duration: 0.22, delay: 0.08, ease: OVERLAY_EASE }
}
>
<PeriodHistoryList
items={items}
isLoading={isLoading}
isError={isError}
labels={labels}
onRetry={onRetry}
/>
</motion.div>
</motion.aside>
</>
)}
</AnimatePresence>
)
}