import { useEffect } from 'react' import { StyleSheet, Text, View } from 'react-native' import { Gesture, GestureDetector } from 'react-native-gesture-handler' import Animated, { cancelAnimation, runOnJS, useAnimatedStyle, useSharedValue, withRepeat, withSequence, withSpring, withTiming, type SharedValue, } from 'react-native-reanimated' import { Ionicons } from '@expo/vector-icons' import type { Marker } from '@repo/types' import type { MapSize } from './FloorMap' const ICON_SIZE = 20 const BADGE_OFFSET = -5 const RING_SIZE = 38 interface MapMarkerProps { marker: Marker mapSize: MapSize mapScale: SharedValue itemCount?: number isSelected?: boolean onPress: () => void onDragEnd: (x: number, y: number) => void } export function MapMarker({ marker, mapSize, mapScale, itemCount = 0, isSelected = false, onPress, onDragEnd }: MapMarkerProps) { const left = marker.x * mapSize.width - ICON_SIZE / 2 const top = marker.y * mapSize.height - ICON_SIZE // 拖动状态 const offsetX = useSharedValue(0) const offsetY = useSharedValue(0) const dragScale = useSharedValue(1) const isDragging = useSharedValue(false) // 对焦脉冲动画 const pulseScale = useSharedValue(1) const pulseOpacity = useSharedValue(0) useEffect(() => { if (isSelected) { // 每次选中从初始状态重新开始脉冲 pulseScale.value = withRepeat( withSequence( withTiming(1, { duration: 0 }), withTiming(2.4, { duration: 1100 }), ), -1, false, ) pulseOpacity.value = withRepeat( withSequence( withTiming(0.7, { duration: 0 }), withTiming(0, { duration: 1100 }), ), -1, false, ) } else { cancelAnimation(pulseScale) cancelAnimation(pulseOpacity) pulseOpacity.value = withTiming(0, { duration: 150 }) } }, [isSelected]) const tap = Gesture.Tap().onEnd(() => { runOnJS(onPress)() }) const pan = Gesture.Pan() .activateAfterLongPress(400) .onStart(() => { dragScale.value = withSpring(1.3) isDragging.value = true }) .onUpdate((e) => { offsetX.value = e.translationX offsetY.value = e.translationY }) .onEnd((e) => { const s = mapScale.value const newX = Math.max(0, Math.min(1, marker.x + e.translationX / (mapSize.width * s))) const newY = Math.max(0, Math.min(1, marker.y + e.translationY / (mapSize.height * s))) runOnJS(onDragEnd)(newX, newY) }) .onFinalize(() => { dragScale.value = withSpring(1) isDragging.value = false offsetX.value = 0 offsetY.value = 0 }) const gesture = Gesture.Race(tap, pan) const containerStyle = useAnimatedStyle(() => ({ transform: [ { translateX: offsetX.value }, { translateY: offsetY.value }, { scale: dragScale.value / mapScale.value }, ], zIndex: isDragging.value ? 999 : isSelected ? 10 : 1, shadowColor: '#000', shadowOpacity: isDragging.value ? 0.3 : 0, shadowRadius: isDragging.value ? 8 : 0, shadowOffset: { width: 0, height: 4 }, elevation: isDragging.value ? 8 : 0, })) const pulseStyle = useAnimatedStyle(() => ({ transform: [{ scale: pulseScale.value }], opacity: pulseOpacity.value, })) const markerColor = marker.color ?? '#3B82F6' return ( {/* 脉冲光环(选中时持续扩散) */} {itemCount > 0 && ( {itemCount} )} ) } const RING_OFFSET = -(RING_SIZE - ICON_SIZE) / 2 // 居中到图标中心 const styles = StyleSheet.create({ container: { position: 'absolute', }, pulseRing: { position: 'absolute', width: RING_SIZE, height: RING_SIZE, borderRadius: RING_SIZE / 2, borderWidth: 2, top: RING_OFFSET, left: RING_OFFSET, }, badge: { position: 'absolute', top: BADGE_OFFSET, right: BADGE_OFFSET, minWidth: 12, height: 12, borderRadius: 6, backgroundColor: '#EF4444', alignItems: 'center', justifyContent: 'center', paddingHorizontal: 2, }, badgeText: { color: '#fff', fontSize: 8, fontWeight: '700', }, })