合入小程序修改内容

This commit is contained in:
HycJack
2026-05-06 03:37:08 +08:00
parent 326e4bac28
commit 1e7e7cc05e
16 changed files with 1072 additions and 450 deletions

View File

@@ -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>
)
}