feat(小程序): 支持设备定位与轨迹展示
This commit is contained in:
@@ -14,17 +14,42 @@
|
||||
font-weight: 700;
|
||||
color: #1A1A1A;
|
||||
display: block;
|
||||
margin-bottom: 12px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 28px;
|
||||
color: #666666;
|
||||
line-height: 1.4;
|
||||
.mode-switch {
|
||||
margin: 0 24px 20px;
|
||||
padding: 8px;
|
||||
background: #FFFFFF;
|
||||
border-radius: 18px;
|
||||
display: flex;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.mode-chip {
|
||||
flex: 1;
|
||||
height: 68px;
|
||||
border-radius: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&.active {
|
||||
background: #FF8C42;
|
||||
|
||||
.mode-chip-text {
|
||||
color: #FFFFFF;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mode-chip-text {
|
||||
font-size: 26px;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.map-section {
|
||||
height: 50vh;
|
||||
position: relative;
|
||||
@@ -36,29 +61,6 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.location-marker {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -100%);
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.marker-pin {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
background: #FF8C42;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 6px 20px rgba(255, 140, 66, 0.4);
|
||||
|
||||
.marker-emoji {
|
||||
font-size: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
.location-card {
|
||||
background: #FFFFFF;
|
||||
border-radius: 20px;
|
||||
@@ -127,12 +129,18 @@
|
||||
font-size: 32px;
|
||||
margin-right: 18px;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.selected {
|
||||
background: #FFF0E6;
|
||||
color: #FF8C42;
|
||||
}
|
||||
}
|
||||
|
||||
.location-info {
|
||||
flex: 1;
|
||||
|
||||
.location-address {
|
||||
.location-address,
|
||||
.location-coordinate {
|
||||
font-size: 30px;
|
||||
font-weight: 600;
|
||||
color: #1A1A1A;
|
||||
@@ -148,3 +156,34 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.location-stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.selected-point-card {
|
||||
border: 2px solid #FFD0B1;
|
||||
}
|
||||
|
||||
.location-empty {
|
||||
background: #F8F9FA;
|
||||
border-radius: 16px;
|
||||
padding: 28px 24px;
|
||||
}
|
||||
|
||||
.location-empty-title {
|
||||
display: block;
|
||||
font-size: 30px;
|
||||
font-weight: 600;
|
||||
color: #1A1A1A;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.location-empty-desc {
|
||||
display: block;
|
||||
font-size: 26px;
|
||||
color: #666666;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@@ -1,82 +1,285 @@
|
||||
import { View, Text, Map } from '@tarojs/components'
|
||||
import { useState } from 'react'
|
||||
import Taro, { useDidShow } from '@tarojs/taro'
|
||||
import { getToken } from '@/services/auth'
|
||||
import {
|
||||
getCurrentDeviceLocation,
|
||||
getDeviceTrajectory,
|
||||
DeviceLocation,
|
||||
DeviceTrajectoryPoint,
|
||||
} from '@/services/location'
|
||||
import './index.scss'
|
||||
|
||||
interface LocationInfo {
|
||||
address: string
|
||||
updateTime: string
|
||||
locationType: string
|
||||
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,
|
||||
}
|
||||
|
||||
interface Coordinates {
|
||||
latitude: number
|
||||
longitude: number
|
||||
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}`
|
||||
}
|
||||
|
||||
// 模拟接口:获取设备位置(返回随机经纬度)
|
||||
const fetchDeviceLocation = async (): Promise<Coordinates> => {
|
||||
// 模拟网络延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 800))
|
||||
function formatAccuracy(value?: number | null): string {
|
||||
if (value === undefined || value === null) return '未知'
|
||||
return `${value} 米`
|
||||
}
|
||||
|
||||
// 在北京范围内生成随机坐标(大致范围)
|
||||
const baseLat = 39.9042
|
||||
const baseLng = 116.4074
|
||||
const randomOffset = () => (Math.random() - 0.5) * 0.1 // 约 ±5km 的偏移
|
||||
function getTodayStartISOString(): string {
|
||||
const now = new Date()
|
||||
now.setHours(0, 0, 0, 0)
|
||||
return now.toISOString()
|
||||
}
|
||||
|
||||
return {
|
||||
latitude: baseLat + randomOffset(),
|
||||
longitude: baseLng + randomOffset()
|
||||
}
|
||||
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 [location, setLocation] = useState<LocationInfo>({
|
||||
address: '阳光社区 3期 附近',
|
||||
updateTime: '刚刚',
|
||||
locationType: '网络定位'
|
||||
})
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [deviceLocation, setDeviceLocation] = useState<DeviceLocation | null>(null)
|
||||
const [trajectory, setTrajectory] = useState<DeviceTrajectoryPoint[]>([])
|
||||
const [selectedPoint, setSelectedPoint] = useState<DeviceTrajectoryPoint | null>(null)
|
||||
const [trajectoryMode, setTrajectoryMode] = useState<TrajectoryMode>('current')
|
||||
const [coordinates, setCoordinates] = useState(DEFAULT_COORDINATES)
|
||||
|
||||
const [coordinates, setCoordinates] = useState<Coordinates>({
|
||||
latitude: 39.9042,
|
||||
longitude: 116.4074
|
||||
})
|
||||
|
||||
// 每次页面显示时获取最新位置
|
||||
useDidShow(() => {
|
||||
loadLocation()
|
||||
void loadLocation()
|
||||
})
|
||||
|
||||
const loadLocation = async () => {
|
||||
try {
|
||||
const coords = await fetchDeviceLocation()
|
||||
setCoordinates(coords)
|
||||
} catch (error) {
|
||||
console.error('获取位置失败:', error)
|
||||
const loadLocation = async (showToast = false, nextMode?: TrajectoryMode) => {
|
||||
const targetMode = nextMode || trajectoryMode
|
||||
|
||||
if (!getToken()) {
|
||||
Taro.reLaunch({ url: '/pages/login/index' })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const refreshLocation = async () => {
|
||||
Taro.showToast({ title: '正在刷新位置...', icon: 'loading' })
|
||||
if (showToast) {
|
||||
Taro.showLoading({ title: '刷新中...' })
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
if (nextMode) {
|
||||
setTrajectoryMode(nextMode)
|
||||
setSelectedPoint(null)
|
||||
}
|
||||
|
||||
try {
|
||||
const coords = await fetchDeviceLocation()
|
||||
setCoordinates(coords)
|
||||
setLocation({
|
||||
...location,
|
||||
updateTime: '刚刚'
|
||||
const currentLocation = await getCurrentDeviceLocation()
|
||||
setDeviceLocation(currentLocation)
|
||||
|
||||
const nextCoordinates = currentLocation
|
||||
? {
|
||||
latitude: currentLocation.lat,
|
||||
longitude: currentLocation.lng,
|
||||
}
|
||||
: DEFAULT_COORDINATES
|
||||
|
||||
let points: DeviceTrajectoryPoint[] = []
|
||||
if (targetMode === 'today') {
|
||||
const response = await getDeviceTrajectory({
|
||||
startAt: getTodayStartISOString(),
|
||||
limit: 200,
|
||||
})
|
||||
points = response?.items || []
|
||||
} else if (targetMode === 'recent') {
|
||||
const response = await getDeviceTrajectory({
|
||||
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',
|
||||
})
|
||||
Taro.showToast({ title: '位置已更新', icon: 'success' })
|
||||
} catch (error) {
|
||||
Taro.showToast({ title: '位置更新失败', 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 (
|
||||
<View className='location-page'>
|
||||
<View className='page-header'>
|
||||
<Text className='page-title'>设备定位</Text>
|
||||
<Text className='page-subtitle'>基于运营商网络基站定位</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'>
|
||||
@@ -85,43 +288,106 @@ export default function Location() {
|
||||
latitude={coordinates.latitude}
|
||||
longitude={coordinates.longitude}
|
||||
scale={15}
|
||||
markers={[{
|
||||
id: 1,
|
||||
latitude: coordinates.latitude,
|
||||
longitude: coordinates.longitude,
|
||||
title: '设备位置',
|
||||
width: 40,
|
||||
height: 40
|
||||
}]}
|
||||
showLocation
|
||||
markers={markers}
|
||||
polyline={polyline}
|
||||
onMarkerTap={handleMarkerTap}
|
||||
onCalloutTap={handleMarkerTap}
|
||||
onError={(event) => {
|
||||
console.error('[location] map error:', event.detail)
|
||||
}}
|
||||
/>
|
||||
<View className='location-marker'>
|
||||
<View className='marker-pin'>
|
||||
<Text className='marker-emoji'>🤖</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className='location-card'>
|
||||
<View className='card-header'>
|
||||
<View className='header-left'>
|
||||
<Text className='location-title'>当前大致位置</Text>
|
||||
<Text className='location-update'>更新于 {location.updateTime} ({location.locationType})</Text>
|
||||
<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={refreshLocation}>
|
||||
<View className='refresh-btn' onClick={() => loadLocation(true)}>
|
||||
<Text className='refresh-icon'>↻</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className='location-detail'>
|
||||
<View className='location-icon'>
|
||||
<Text>📍</Text>
|
||||
{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>
|
||||
<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>
|
||||
</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>
|
||||
)}
|
||||
|
||||
<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-info'>
|
||||
<Text className='location-address'>{location.address}</Text>
|
||||
<Text className='location-desc'>通过设备 4G 信号基站解析获得,误差范围约 50-200 米。</Text>
|
||||
) : (
|
||||
<View className='location-empty'>
|
||||
<Text className='location-empty-title'>暂无轨迹</Text>
|
||||
<Text className='location-empty-desc'>
|
||||
{trajectoryMode === 'today' ? '今天还没有新的历史点位。' : '最近时间范围内没有新的历史点位。'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
|
||||
67
banban-mini/src/services/location.ts
Normal file
67
banban-mini/src/services/location.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { request } from './api'
|
||||
import { resolveActiveBinding } from './binding'
|
||||
|
||||
export interface DeviceLocation {
|
||||
child_id: number
|
||||
child_name?: string | null
|
||||
device_id: string
|
||||
coord_type: string
|
||||
lat: number
|
||||
lng: number
|
||||
accuracy_m?: number | null
|
||||
altitude_m?: number | null
|
||||
speed_mps?: number | null
|
||||
heading_deg?: number | null
|
||||
source: number
|
||||
battery_pct?: number | null
|
||||
device_time: string
|
||||
server_time: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface DeviceTrajectoryPoint extends DeviceLocation {
|
||||
id: number
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface DeviceTrajectoryResponse {
|
||||
items: DeviceTrajectoryPoint[]
|
||||
total: number
|
||||
start_at?: string | null
|
||||
end_at?: string | null
|
||||
}
|
||||
|
||||
export async function getCurrentDeviceLocation(deviceId?: string): Promise<DeviceLocation | null> {
|
||||
const resolvedDeviceId = String(deviceId || '').trim() || (await resolveActiveBinding())?.device_id || ''
|
||||
if (!resolvedDeviceId) return null
|
||||
|
||||
try {
|
||||
return await request<DeviceLocation>(`/devices/${resolvedDeviceId}/location`)
|
||||
} catch (error: any) {
|
||||
if (error?.status === 404) return null
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export async function getDeviceTrajectory(params?: {
|
||||
deviceId?: string
|
||||
startAt?: string
|
||||
endAt?: string
|
||||
limit?: number
|
||||
}): Promise<DeviceTrajectoryResponse | null> {
|
||||
const resolvedDeviceId = String(params?.deviceId || '').trim() || (await resolveActiveBinding())?.device_id || ''
|
||||
if (!resolvedDeviceId) return null
|
||||
|
||||
const query = new URLSearchParams()
|
||||
if (params?.startAt) query.append('start_at', params.startAt)
|
||||
if (params?.endAt) query.append('end_at', params.endAt)
|
||||
if (params?.limit) query.append('limit', String(params.limit))
|
||||
|
||||
try {
|
||||
const suffix = query.toString() ? `?${query.toString()}` : ''
|
||||
return await request<DeviceTrajectoryResponse>(`/devices/${resolvedDeviceId}/trajectory${suffix}`)
|
||||
} catch (error: any) {
|
||||
if (error?.status === 404) return null
|
||||
throw error
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user