import { View, Text, Map } from '@tarojs/components' import { useState } from 'react' import Taro, { useDidShow } from '@tarojs/taro' import { getToken } from '@/services/auth' import { loadCurrentChildBindingContext } from '@/services/binding' import { getDeviceOnlineStatus } from '@/services/device' import { getCurrentDeviceLocation, getDeviceTrajectory, DeviceLocation, DeviceTrajectoryPoint, } from '@/services/location' import { useSystemBanner } from '@/components/system-banner/use-system-banner' import './index.scss' type TrajectoryMode = 'current' | 'today' | 'recent' const DEVICE_MARKER_ID = 1 const TRAJECTORY_MARKER_BASE_ID = 1000 const DEVICE_MARKER_ICON = require('../../assets/tab-icons/location-selected.png') const START_MARKER_ICON = require('../../assets/tab-icons/device-icon.png') const END_MARKER_ICON = require('../../assets/tab-icons/device-selected.png') const POINT_MARKER_ICON = require('../../assets/tab-icons/location-icon.png') const DEFAULT_COORDINATES = { latitude: 39.9042, longitude: 116.4074, } const MODE_OPTIONS: Array<{ key: TrajectoryMode; label: string }> = [ { key: 'current', label: '当前位置' }, { key: 'today', label: '今日轨迹' }, { key: 'recent', label: '最近点位' }, ] function formatTime(value?: string | null): string { if (!value) return '--' const date = new Date(value) if (Number.isNaN(date.getTime())) return value const month = `${date.getMonth() + 1}`.padStart(2, '0') const day = `${date.getDate()}`.padStart(2, '0') const hour = `${date.getHours()}`.padStart(2, '0') const minute = `${date.getMinutes()}`.padStart(2, '0') return `${month}-${day} ${hour}:${minute}` } function formatAccuracy(value?: number | null): string { if (value === undefined || value === null) return '未知' return `${value} 米` } function formatOptionalNumber(value?: number | null, digits = 1): string | null { if (value === undefined || value === null) return null return Number(value).toFixed(digits) } function buildLocationDetailLines( point: DeviceLocation | DeviceTrajectoryPoint, options?: { includeIdentity?: boolean } ): string[] { const lines: string[] = [] if (options?.includeIdentity) { lines.push(`设备 ${point.device_id} · ${point.child_name || '未命名儿童'}`) } lines.push(`上报时间 ${formatTime(point.device_time)}`) if (point.server_time) { lines.push(`服务端时间 ${formatTime(point.server_time)}`) } if (point.coord_type) { lines.push(`坐标系 ${point.coord_type}`) } if (point.accuracy_m !== null && point.accuracy_m !== undefined) { lines.push(`定位精度 ${formatAccuracy(point.accuracy_m)}`) } const altitudeText = formatOptionalNumber(point.altitude_m) if (altitudeText) { lines.push(`海拔 ${altitudeText} 米`) } const speedText = formatOptionalNumber(point.speed_mps) if (speedText) { lines.push(`速度 ${speedText} 米/秒`) } if (point.heading_deg !== null && point.heading_deg !== undefined) { lines.push(`方向 ${point.heading_deg}°`) } if (point.source !== null && point.source !== undefined && point.source > 0) { lines.push(`定位来源 ${point.source}`) } if (point.battery_pct !== null && point.battery_pct !== undefined) { lines.push(`设备电量 ${point.battery_pct}%`) } return lines } function getTodayStartISOString(): string { const now = new Date() now.setHours(0, 0, 0, 0) return now.toISOString() } function getRecentStartISOString(hours = 24): string { return new Date(Date.now() - hours * 60 * 60 * 1000).toISOString() } function getMarkerLabel(point: DeviceTrajectoryPoint | DeviceLocation | null): string { if (!point) return '' return point.child_name || point.device_id } function getTrajectoryMarkerId(index: number): number { return TRAJECTORY_MARKER_BASE_ID + index } export default function Location() { const systemBanner = useSystemBanner() const [loading, setLoading] = useState(true) const [deviceLocation, setDeviceLocation] = useState(null) const [trajectory, setTrajectory] = useState([]) const [selectedPoint, setSelectedPoint] = useState(null) const [trajectoryMode, setTrajectoryMode] = useState('current') const [coordinates, setCoordinates] = useState(DEFAULT_COORDINATES) const [locationNotice, setLocationNotice] = useState(null) const [emptyState, setEmptyState] = useState<{ title: string; desc: string; actionText: string; actionUrl: string } | null>( null ) useDidShow(() => { void loadLocation() }) const loadLocation = async (showToast = false, nextMode?: TrajectoryMode) => { const targetMode = nextMode || trajectoryMode if (!getToken()) { Taro.reLaunch({ url: '/pages/login/index' }) return } if (showToast) { Taro.showLoading({ title: '刷新中...' }) } setLoading(true) setEmptyState(null) setLocationNotice(null) if (nextMode) { setTrajectoryMode(nextMode) setSelectedPoint(null) } try { const context = await loadCurrentChildBindingContext() if (!context.currentChild) { setDeviceLocation(null) setTrajectory([]) setSelectedPoint(null) setCoordinates(DEFAULT_COORDINATES) setEmptyState({ title: '还没有当前孩子', desc: '请先去设置与管理页创建或选择一个孩子,再查看定位。', actionText: '去设置与管理', actionUrl: '/pages/sleep/index', }) return } const resolvedDeviceId = context.currentBinding?.device_id || '' if (!resolvedDeviceId) { setDeviceLocation(null) setTrajectory([]) setSelectedPoint(null) setCoordinates(DEFAULT_COORDINATES) setEmptyState({ title: '当前孩子还没有绑定设备', desc: '先给当前孩子绑定一台设备,定位和轨迹页才会有内容。', actionText: '去绑定设备', actionUrl: '/pages/bind/index', }) return } const onlineStatus = await getDeviceOnlineStatus(resolvedDeviceId) if (onlineStatus && !onlineStatus.online) { setDeviceLocation(null) setTrajectory([]) setSelectedPoint(null) setCoordinates(DEFAULT_COORDINATES) setLocationNotice(onlineStatus.message || '设备不在线或暂时无法上报位置') return } const currentLocation = await getCurrentDeviceLocation(resolvedDeviceId) setDeviceLocation(currentLocation) const nextCoordinates = currentLocation ? { latitude: currentLocation.lat, longitude: currentLocation.lng, } : DEFAULT_COORDINATES let points: DeviceTrajectoryPoint[] = [] if (targetMode === 'today') { const response = await getDeviceTrajectory({ deviceId: resolvedDeviceId, startAt: getTodayStartISOString(), limit: 200, }) points = response?.items || [] } else if (targetMode === 'recent') { const response = await getDeviceTrajectory({ deviceId: resolvedDeviceId, startAt: getRecentStartISOString(72), limit: 100, }) points = response?.items || [] } setTrajectory(points) setSelectedPoint(points.length > 0 ? points[points.length - 1] : null) const focusPoint = points.length > 0 ? points[points.length - 1] : currentLocation if (focusPoint) { setCoordinates({ latitude: focusPoint.lat, longitude: focusPoint.lng, }) } else { setCoordinates(nextCoordinates) } if (showToast) { Taro.showToast({ title: currentLocation || points.length > 0 ? '位置已更新' : '暂无设备位置', icon: currentLocation || points.length > 0 ? 'success' : 'none', }) } } catch (error: any) { console.error('[location] load failed:', error) Taro.showToast({ title: error?.message || '位置更新失败', icon: 'none', }) } finally { if (showToast) { Taro.hideLoading() } setLoading(false) } } const handleMarkerTap = (event: any) => { const markerId = Number(event?.detail?.markerId) if (markerId === DEVICE_MARKER_ID && deviceLocation) { setCoordinates({ latitude: deviceLocation.lat, longitude: deviceLocation.lng, }) return } const pointIndex = markerId - TRAJECTORY_MARKER_BASE_ID const point = trajectory[pointIndex] if (!point) return setSelectedPoint(point) setCoordinates({ latitude: point.lat, longitude: point.lng, }) } const currentMarker = deviceLocation && trajectoryMode === 'current' ? [ { id: DEVICE_MARKER_ID, latitude: deviceLocation.lat, longitude: deviceLocation.lng, title: getMarkerLabel(deviceLocation), iconPath: DEVICE_MARKER_ICON, width: 40, height: 40, callout: { content: `当前位置\n${formatTime(deviceLocation.device_time)}`, color: '#1A1A1A', fontSize: 13, anchorX: 0, anchorY: -4, borderRadius: 8, borderWidth: 1, borderColor: '#FF8C42', bgColor: '#FFFFFF', padding: 8, display: 'BYCLICK' as const, textAlign: 'center' as const, }, }, ] : [] const trajectoryMarkers = trajectory.length > 0 ? trajectory.map((point, index) => { const isFirst = index === 0 const isLast = index === trajectory.length - 1 const isSelected = selectedPoint?.id === point.id return { id: getTrajectoryMarkerId(index), latitude: point.lat, longitude: point.lng, title: `${isFirst ? '起点' : isLast ? '终点' : '轨迹点'} ${formatTime(point.device_time)}`, iconPath: isFirst ? START_MARKER_ICON : isLast ? END_MARKER_ICON : POINT_MARKER_ICON, width: isSelected ? 36 : isFirst || isLast ? 32 : 22, height: isSelected ? 36 : isFirst || isLast ? 32 : 22, zIndex: isSelected ? 20 : isFirst || isLast ? 12 : 8, callout: { content: `${isFirst ? '起点' : isLast ? '终点' : '轨迹点'}\n${formatTime(point.device_time)}`, color: '#1A1A1A', fontSize: 13, anchorX: 0, anchorY: -4, borderRadius: 8, borderWidth: 1, borderColor: isSelected ? '#FF8C42' : '#E6E6E6', bgColor: '#FFFFFF', padding: 8, display: isSelected ? ('ALWAYS' as const) : ('BYCLICK' as const), textAlign: 'center' as const, }, } }) : [] const markers = trajectoryMode === 'current' ? currentMarker : trajectoryMarkers const polyline = trajectory.length >= 2 ? [ { points: trajectory.map((item) => ({ latitude: item.lat, longitude: item.lng, })), color: '#FF8C42', width: 6, dottedLine: false, }, ] : [] const summaryTitle = trajectoryMode === 'current' ? '当前设备坐标' : trajectoryMode === 'today' ? '今日轨迹' : '最近点位' return ( 设备定位 {emptyState ? ( {emptyState.title} {emptyState.desc} { if (emptyState.actionUrl === '/pages/sleep/index') { Taro.switchTab({ url: emptyState.actionUrl }) return } Taro.navigateTo({ url: emptyState.actionUrl }) }} > {emptyState.actionText} ) : ( <> {/* {MODE_OPTIONS.map((item) => ( { if (trajectoryMode === item.key) return void loadLocation(false, item.key) }} > {item.label} ))} */} { console.error('[location] map error:', event.detail) }} /> {summaryTitle} {locationNotice ? locationNotice : loading ? '正在获取设备位置...' : trajectoryMode === 'current' ? `更新于 ${formatTime(deviceLocation?.updated_at)}` : `轨迹点 ${trajectory.length} 个 · 最近更新 ${formatTime(trajectory[trajectory.length - 1]?.device_time)}`} loadLocation(true)}> {trajectoryMode === 'current' ? ( deviceLocation ? ( 📍 {deviceLocation.lat.toFixed(6)}, {deviceLocation.lng.toFixed(6)} {buildLocationDetailLines(deviceLocation, { includeIdentity: true }).map((line, index) => ( {line} ))} ) : ( {locationNotice || '当前孩子暂无设备位置'} {locationNotice && 请确认设备已开机并保持网络可用。} ) ) : trajectory.length > 0 ? ( {selectedPoint && ( {selectedPoint.lat.toFixed(6)}, {selectedPoint.lng.toFixed(6)} {buildLocationDetailLines(selectedPoint).map((line, index) => ( {line} ))} )} 🛣️ 共 {trajectory.length} 个轨迹点 起点 {formatTime(trajectory[0].device_time)} · {trajectory[0].lat.toFixed(6)}, {trajectory[0].lng.toFixed(6)} 终点 {formatTime(trajectory[trajectory.length - 1].device_time)} ·{' '} {trajectory[trajectory.length - 1].lat.toFixed(6)}, {trajectory[trajectory.length - 1].lng.toFixed(6)} ) : ( 暂无轨迹 {trajectoryMode === 'today' ? '当前孩子今天还没有新的历史点位。' : '当前孩子最近没有新的历史点位。'} )} )} {systemBanner} ) }