合入小程序修改内容
This commit is contained in:
@@ -2,6 +2,7 @@ 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 {
|
||||
getCurrentDeviceLocation,
|
||||
getDeviceTrajectory,
|
||||
@@ -26,8 +27,8 @@ const DEFAULT_COORDINATES = {
|
||||
|
||||
const MODE_OPTIONS: Array<{ key: TrajectoryMode; label: string }> = [
|
||||
{ key: 'current', label: '当前位置' },
|
||||
// { key: 'today', label: '今日轨迹' },
|
||||
// { key: 'recent', label: '最近点位' },
|
||||
{ key: 'today', label: '今日轨迹' },
|
||||
{ key: 'recent', label: '最近点位' },
|
||||
]
|
||||
|
||||
function formatTime(value?: string | null): string {
|
||||
@@ -46,6 +47,56 @@ function formatAccuracy(value?: number | null): string {
|
||||
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)
|
||||
@@ -72,6 +123,9 @@ export default function Location() {
|
||||
const [selectedPoint, setSelectedPoint] = useState<DeviceTrajectoryPoint | null>(null)
|
||||
const [trajectoryMode, setTrajectoryMode] = useState<TrajectoryMode>('current')
|
||||
const [coordinates, setCoordinates] = useState(DEFAULT_COORDINATES)
|
||||
const [emptyState, setEmptyState] = useState<{ title: string; desc: string; actionText: string; actionUrl: string } | null>(
|
||||
null
|
||||
)
|
||||
|
||||
useDidShow(() => {
|
||||
void loadLocation()
|
||||
@@ -90,13 +144,44 @@ export default function Location() {
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
setEmptyState(null)
|
||||
if (nextMode) {
|
||||
setTrajectoryMode(nextMode)
|
||||
setSelectedPoint(null)
|
||||
}
|
||||
|
||||
try {
|
||||
const currentLocation = await getCurrentDeviceLocation()
|
||||
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 currentLocation = await getCurrentDeviceLocation(resolvedDeviceId)
|
||||
setDeviceLocation(currentLocation)
|
||||
|
||||
const nextCoordinates = currentLocation
|
||||
@@ -109,12 +194,14 @@ export default function Location() {
|
||||
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,
|
||||
})
|
||||
@@ -267,128 +354,144 @@ export default function Location() {
|
||||
<Text className='page-title'>设备定位</Text>
|
||||
</View>
|
||||
|
||||
<View className='mode-switch'>
|
||||
{MODE_OPTIONS.map((item) => (
|
||||
<View
|
||||
key={item.key}
|
||||
className={`mode-chip ${trajectoryMode === item.key ? 'active' : ''}`}
|
||||
onClick={() => {
|
||||
if (trajectoryMode === item.key) return
|
||||
void loadLocation(false, item.key)
|
||||
}}
|
||||
>
|
||||
<Text className='mode-chip-text'>{item.label}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<View className='map-section'>
|
||||
<Map
|
||||
className='map'
|
||||
latitude={coordinates.latitude}
|
||||
longitude={coordinates.longitude}
|
||||
scale={15}
|
||||
markers={markers}
|
||||
polyline={polyline}
|
||||
onMarkerTap={handleMarkerTap}
|
||||
onCalloutTap={handleMarkerTap}
|
||||
onError={(event) => {
|
||||
console.error('[location] map error:', event.detail)
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View className='location-card'>
|
||||
<View className='card-header'>
|
||||
<View className='header-left'>
|
||||
<Text className='location-title'>{summaryTitle}</Text>
|
||||
<Text className='location-update'>
|
||||
{loading
|
||||
? '正在获取设备位置...'
|
||||
: trajectoryMode === 'current'
|
||||
? `更新于 ${formatTime(deviceLocation?.updated_at)}`
|
||||
: `轨迹点 ${trajectory.length} 个 · 最近更新 ${formatTime(trajectory[trajectory.length - 1]?.device_time)}`}
|
||||
</Text>
|
||||
</View>
|
||||
<View className='refresh-btn' onClick={() => loadLocation(true)}>
|
||||
<Text className='refresh-icon'>↻</Text>
|
||||
{emptyState ? (
|
||||
<View className='location-empty-shell'>
|
||||
<View className='location-empty-card'>
|
||||
<Text className='location-empty-title'>{emptyState.title}</Text>
|
||||
<Text className='location-empty-desc'>{emptyState.desc}</Text>
|
||||
<View
|
||||
className='location-empty-action'
|
||||
onClick={() => {
|
||||
if (emptyState.actionUrl === '/pages/sleep/index') {
|
||||
Taro.switchTab({ url: emptyState.actionUrl })
|
||||
return
|
||||
}
|
||||
Taro.navigateTo({ url: emptyState.actionUrl })
|
||||
}}
|
||||
>
|
||||
<Text className='location-empty-action-text'>{emptyState.actionText}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
<View className='mode-switch'>
|
||||
{MODE_OPTIONS.map((item) => (
|
||||
<View
|
||||
key={item.key}
|
||||
className={`mode-chip ${trajectoryMode === item.key ? 'active' : ''}`}
|
||||
onClick={() => {
|
||||
if (trajectoryMode === item.key) return
|
||||
void loadLocation(false, item.key)
|
||||
}}
|
||||
>
|
||||
<Text className='mode-chip-text'>{item.label}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{trajectoryMode === 'current' ? (
|
||||
deviceLocation ? (
|
||||
<View className='location-detail'>
|
||||
<View className='location-icon'>
|
||||
<Text>📍</Text>
|
||||
<View className='map-section'>
|
||||
<Map
|
||||
className='map'
|
||||
latitude={coordinates.latitude}
|
||||
longitude={coordinates.longitude}
|
||||
scale={15}
|
||||
markers={markers}
|
||||
polyline={polyline}
|
||||
onMarkerTap={handleMarkerTap}
|
||||
onCalloutTap={handleMarkerTap}
|
||||
onError={(event) => {
|
||||
console.error('[location] map error:', event.detail)
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View className='location-card'>
|
||||
<View className='card-header'>
|
||||
<View className='header-left'>
|
||||
<Text className='location-title'>{summaryTitle}</Text>
|
||||
<Text className='location-update'>
|
||||
{loading
|
||||
? '正在获取设备位置...'
|
||||
: trajectoryMode === 'current'
|
||||
? `更新于 ${formatTime(deviceLocation?.updated_at)}`
|
||||
: `轨迹点 ${trajectory.length} 个 · 最近更新 ${formatTime(trajectory[trajectory.length - 1]?.device_time)}`}
|
||||
</Text>
|
||||
</View>
|
||||
<View className='location-info'>
|
||||
<Text className='location-coordinate'>
|
||||
{deviceLocation.lat.toFixed(6)}, {deviceLocation.lng.toFixed(6)}
|
||||
</Text>
|
||||
<Text className='location-desc'>
|
||||
设备 {deviceLocation.device_id} · {deviceLocation.child_name || '未命名儿童'}
|
||||
</Text>
|
||||
<Text className='location-desc'>上报时间 {formatTime(deviceLocation.device_time)}</Text>
|
||||
<Text className='location-desc'>
|
||||
坐标系 {deviceLocation.coord_type} · 精度 {formatAccuracy(deviceLocation.accuracy_m)}
|
||||
</Text>
|
||||
{deviceLocation.battery_pct !== null && deviceLocation.battery_pct !== undefined && (
|
||||
<Text className='location-desc'>设备电量 {deviceLocation.battery_pct}%</Text>
|
||||
<View className='refresh-btn' onClick={() => loadLocation(true)}>
|
||||
<Text className='refresh-icon'>↻</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{trajectoryMode === 'current' ? (
|
||||
deviceLocation ? (
|
||||
<View className='location-detail'>
|
||||
<View className='location-icon'>
|
||||
<Text>📍</Text>
|
||||
</View>
|
||||
<View className='location-info'>
|
||||
<Text className='location-coordinate'>
|
||||
{deviceLocation.lat.toFixed(6)}, {deviceLocation.lng.toFixed(6)}
|
||||
</Text>
|
||||
{buildLocationDetailLines(deviceLocation, { includeIdentity: true }).map((line, index) => (
|
||||
<Text key={`current-${index}`} className='location-desc'>
|
||||
{line}
|
||||
</Text>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<View className='location-empty'>
|
||||
<Text className='location-empty-title'>当前孩子暂无设备位置</Text>
|
||||
</View>
|
||||
)
|
||||
) : trajectory.length > 0 ? (
|
||||
<View className='location-stack'>
|
||||
{selectedPoint && (
|
||||
<View className='location-detail selected-point-card'>
|
||||
<View className='location-icon selected'>
|
||||
<Text>●</Text>
|
||||
</View>
|
||||
<View className='location-info'>
|
||||
<Text className='location-coordinate'>
|
||||
{selectedPoint.lat.toFixed(6)}, {selectedPoint.lng.toFixed(6)}
|
||||
</Text>
|
||||
{buildLocationDetailLines(selectedPoint).map((line, index) => (
|
||||
<Text key={`selected-${index}`} className='location-desc'>
|
||||
{line}
|
||||
</Text>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View className='location-detail'>
|
||||
<View className='location-icon'>
|
||||
<Text>🛣️</Text>
|
||||
</View>
|
||||
<View className='location-info'>
|
||||
<Text className='location-coordinate'>共 {trajectory.length} 个轨迹点</Text>
|
||||
<Text className='location-desc'>
|
||||
起点 {formatTime(trajectory[0].device_time)} · {trajectory[0].lat.toFixed(6)}, {trajectory[0].lng.toFixed(6)}
|
||||
</Text>
|
||||
<Text className='location-desc'>
|
||||
终点 {formatTime(trajectory[trajectory.length - 1].device_time)} ·{' '}
|
||||
{trajectory[trajectory.length - 1].lat.toFixed(6)}, {trajectory[trajectory.length - 1].lng.toFixed(6)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<View className='location-empty'>
|
||||
<Text className='location-empty-title'>当前孩子暂无设备位置</Text>
|
||||
</View>
|
||||
)
|
||||
) : trajectory.length > 0 ? (
|
||||
<View className='location-stack'>
|
||||
{selectedPoint && (
|
||||
<View className='location-detail selected-point-card'>
|
||||
<View className='location-icon selected'>
|
||||
<Text>●</Text>
|
||||
</View>
|
||||
<View className='location-info'>
|
||||
<Text className='location-coordinate'>
|
||||
{selectedPoint.lat.toFixed(6)}, {selectedPoint.lng.toFixed(6)}
|
||||
</Text>
|
||||
<Text className='location-desc'>上报时间 {formatTime(selectedPoint.device_time)}</Text>
|
||||
<Text className='location-desc'>
|
||||
坐标系 {selectedPoint.coord_type} · 精度 {formatAccuracy(selectedPoint.accuracy_m)}
|
||||
</Text>
|
||||
{selectedPoint.battery_pct !== null && selectedPoint.battery_pct !== undefined && (
|
||||
<Text className='location-desc'>设备电量 {selectedPoint.battery_pct}%</Text>
|
||||
)}
|
||||
</View>
|
||||
) : (
|
||||
<View className='location-empty'>
|
||||
<Text className='location-empty-title'>暂无轨迹</Text>
|
||||
<Text className='location-empty-desc'>
|
||||
{trajectoryMode === 'today' ? '当前孩子今天还没有新的历史点位。' : '当前孩子最近没有新的历史点位。'}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View className='location-detail'>
|
||||
<View className='location-icon'>
|
||||
<Text>🛣️</Text>
|
||||
</View>
|
||||
<View className='location-info'>
|
||||
<Text className='location-coordinate'>共 {trajectory.length} 个轨迹点</Text>
|
||||
<Text className='location-desc'>
|
||||
起点 {formatTime(trajectory[0].device_time)} · {trajectory[0].lat.toFixed(6)}, {trajectory[0].lng.toFixed(6)}
|
||||
</Text>
|
||||
<Text className='location-desc'>
|
||||
终点 {formatTime(trajectory[trajectory.length - 1].device_time)} ·{' '}
|
||||
{trajectory[trajectory.length - 1].lat.toFixed(6)}, {trajectory[trajectory.length - 1].lng.toFixed(6)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<View className='location-empty'>
|
||||
<Text className='location-empty-title'>暂无轨迹</Text>
|
||||
<Text className='location-empty-desc'>
|
||||
{trajectoryMode === 'today' ? '当前孩子今天还没有新的历史点位。' : '当前孩子最近没有新的历史点位。'}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user