import { useState, useEffect, useMemo, useCallback, useRef } from 'react' import { createPortal } from 'react-dom' import { AnimatePresence, motion } from 'framer-motion' import { cn, getStaticImage, getPublicPath } from '@/lib/utils' import { Image, Video, DragFill } from '@/components' import type { DragFillResult } from '@/components' import { useScale } from '@/contexts/ScaleContext' import type { VideoRef } from '@/components/video' export interface ViseAssemblyDialogProps { /** 是否可见 */ visible?: boolean /** 关闭回调 */ onClose?: () => void /** 完成回调 */ onComplete?: () => void } type PartType = 'A' | 'B' | 'C' /** * 组装台钳互动弹窗 * - 顶部三个位置用于放置零件 * - 底部三个零件(A、B、C)可以选择 * - 答题成功后播放视频 */ const ViseAssemblyDialog = ({ visible = true, onClose, onComplete }: ViseAssemblyDialogProps) => { const maskSrc = getStaticImage('components/蒙版遮罩') const backSrc = getStaticImage('components/关闭') // 零件背景提示素材 const partHintA = getStaticImage('huangyanpei/reflection/scene3/11_005') const partHintB = getStaticImage('huangyanpei/reflection/scene3/11_006') const partHintC = getStaticImage('huangyanpei/reflection/scene3/c') // 零件素材 const partA = getStaticImage('huangyanpei/reflection/scene3/11_010') const partB = getStaticImage('huangyanpei/reflection/scene3/11_011') const partC = getStaticImage('huangyanpei/reflection/scene3/11_012') const { scale, offsetX, offsetY, DESIGN_WIDTH, DESIGN_HEIGHT } = useScale() const [mounted, setMounted] = useState(false) const [value, setValue] = useState([]) const [videoVisible, setVideoVisible] = useState(false) const videoRef = useRef(null) const completedRef = useRef(false) // SSR 兼容 useEffect(() => { setMounted(true) }, []) // 重置游戏状态 useEffect(() => { if (visible) { setValue([]) completedRef.current = false setVideoVisible(false) } }, [visible]) const handleClose = useCallback( (e: React.MouseEvent) => { e.stopPropagation() if (!videoVisible) { onClose?.() } }, [onClose, videoVisible] ) // 转换为 DragFill 的 options const options = useMemo( () => [ { value: 'A', label: '丝杠和丝杆' }, { value: 'B', label: '活动钳口' }, { value: 'C', label: '固定钳口和基座' }, ], [] ) // 转换为 DragFill 的 blanks const blanks = useMemo( () => [ { id: 'slot0', label: '位置1' }, { id: 'slot1', label: '位置2' }, { id: 'slot2', label: '位置3' }, ], [] ) // 处理拖拽结果变化 const handleValueChange = useCallback((newValue: DragFillResult[]) => { // 验证:只允许 A->slot0, B->slot1, C->slot2 const filteredValue = newValue.filter((result) => { const expectedSlot = result.optionValue === 'A' ? 'slot0' : result.optionValue === 'B' ? 'slot1' : 'slot2' return result.blankId === expectedSlot }) setValue(filteredValue) }, []) // 处理"拼接台钳"按钮点击 const handleAssemble = useCallback(() => { if (value.length < 3) { return } const slot0 = value.find((r) => r.blankId === 'slot0') const slot1 = value.find((r) => r.blankId === 'slot1') const slot2 = value.find((r) => r.blankId === 'slot2') // 验证:A->slot0, B->slot1, C->slot2 const isCorrect = slot0?.optionValue === 'A' && slot1?.optionValue === 'B' && slot2?.optionValue === 'C' if (isCorrect) { completedRef.current = true // 答题成功,播放视频 setVideoVisible(true) } else { // 答题失败,清除错误的填充 setTimeout(() => { setValue([]) }, 1500) alert('组装顺序不正确,请重新尝试!') } }, [value]) const handleVideoEnd = useCallback(() => { setVideoVisible(false) onComplete?.() onClose?.() }, [onComplete, onClose]) // 样式 const overlayStyle = useMemo( () => ({ position: 'fixed', inset: 0, zIndex: 9999, pointerEvents: 'none', }), [] ) const scaledContainerStyle = useMemo( () => ({ position: 'absolute', width: `${DESIGN_WIDTH}px`, height: `${DESIGN_HEIGHT}px`, left: `${offsetX}px`, top: `${offsetY}px`, transform: `scale(${scale})`, transformOrigin: 'top left', overflow: 'hidden', }), [scale, offsetX, offsetY, DESIGN_WIDTH, DESIGN_HEIGHT] ) if (!mounted) return null const content = ( {visible && (
{/* 背景蒙版 */} {maskSrc && ( )} {/* 返回按钮 */} {onClose && !videoVisible && ( 返回 )} {/* 视频播放层 */} {videoVisible ? (
) : ( /* 内容区域 */
{/* 顶部三个位置 - 更大的框 */}
{[0, 1, 2].map((index) => { const blankId = `slot${index}` const filled = value.find((r) => r.blankId === blankId) const partType = filled ? (filled.optionValue as PartType) : null const hasPart = partType !== null return (
{/* 背景提示 */} {!hasPart && index === 0 && ( 位置A提示 )} {!hasPart && index === 1 && ( 位置B提示 )} {!hasPart && index === 2 && ( 位置C提示 )} {/* 已放置的零件 */} {partType === 'A' && ( 零件A )} {partType === 'B' && ( 零件B )} {partType === 'C' && ( 零件C )}
) })}
{/* 中间的"拼接台钳"按钮 */} {/* 底部零件选择区域 - 使用 DragFill */}
零件区
{ const partSrc = option.value === 'A' ? partA : option.value === 'B' ? partB : partC return (
{option.value}
{`零件${option.value}`}
{option.label}
) }} renderDragging={(option) => { const partSrc = option.value === 'A' ? partA : option.value === 'B' ? partB : partC return (
{`零件${option.value}`}
) }} renderBlank={() => { // 返回隐藏元素,实际的空白区域在页面中通过 data-blank-id 定位 return
}} />
)}
)} ) return createPortal(content, document.body) } export default ViseAssemblyDialog