1104 lines
26 KiB
Plaintext
1104 lines
26 KiB
Plaintext
<!-- 手环位置详情页面 -->
|
|
<template>
|
|
<scroll-view direction="vertical" class="location-page" :scroll-y="true" :enable-back-to-top="true">
|
|
<!-- 页面标题 -->
|
|
<view class="header">
|
|
<text class="title">手环位置</text>
|
|
<text class="subtitle">实时位置与围栏管理</text>
|
|
</view>
|
|
|
|
<!-- 当前位置卡片 -->
|
|
<view class="current-location-section">
|
|
<view class="location-card">
|
|
<view class="card-header">
|
|
<text class="card-title">当前位置</text>
|
|
<view class="status-indicator" :class="{ 'online': currentLocation?.status == 'online', 'offline': currentLocation?.status == 'offline' }">
|
|
<text class="status-dot">●</text>
|
|
<text class="status-text">{{ getStatusText(currentLocation?.status ?? 'unknown') }}</text>
|
|
</view>
|
|
</view>
|
|
|
|
<view v-if="locationLoading" class="loading-container">
|
|
<text class="loading-text">正在获取位置...</text>
|
|
</view>
|
|
<view v-else-if="currentLocation != null" class="location-content">
|
|
<!-- 基站信息 -->
|
|
<view v-if="currentLocation?.baseStation != null" class="base-station-info">
|
|
<view class="info-row">
|
|
<text class="info-label">基站</text>
|
|
<text class="info-value">{{ getBaseStationName(currentLocation?.baseStation) }}</text>
|
|
</view>
|
|
<view class="info-row">
|
|
<text class="info-label">位置</text>
|
|
<text class="info-value">{{ getBaseStationLocation(currentLocation?.baseStation) }}</text>
|
|
</view>
|
|
<view class="info-row">
|
|
<text class="info-label">信号强度</text>
|
|
<text class="info-value">{{ getBaseStationSignalStrength(currentLocation?.baseStation) }}dBm</text>
|
|
</view>
|
|
<view class="info-row">
|
|
<text class="info-label">覆盖范围</text>
|
|
<text class="info-value">{{ getBaseStationRange(currentLocation?.baseStation) }}米</text>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 估算位置 -->
|
|
<view v-if="currentLocation?.estimatedLocation != null" class="estimated-location">
|
|
<text class="section-label">估算坐标</text>
|
|
<view class="coordinates">
|
|
<text class="coordinate-text">经度: {{ getEstimatedLongitude(currentLocation?.estimatedLocation) }}</text>
|
|
<text class="coordinate-text">纬度: {{ getEstimatedLatitude(currentLocation?.estimatedLocation) }}</text>
|
|
<text class="accuracy-text">精度: ±{{ getEstimatedAccuracy(currentLocation?.estimatedLocation) }}米</text>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 最后更新时间 -->
|
|
<view class="update-info">
|
|
<text class="update-text">更新时间: {{ formatDateTime(currentLocation?.lastUpdate) }}</text>
|
|
</view>
|
|
</view>
|
|
|
|
<view v-else class="error-container">
|
|
<text class="error-text">无法获取位置信息</text>
|
|
</view>
|
|
|
|
<button class="refresh-btn" @click="refreshLocation" :disabled="locationLoading">
|
|
<text class="btn-text">{{ locationLoading ? '刷新中...' : '刷新位置' }}</text>
|
|
</button>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 基站列表 -->
|
|
<view class="base-stations-section">
|
|
<view class="section-header">
|
|
<text class="section-title">附近基站</text>
|
|
<button class="refresh-small-btn" @click="refreshBaseStations" :disabled="baseStationsLoading">
|
|
<text class="small-btn-text">刷新</text>
|
|
</button>
|
|
</view>
|
|
|
|
<view v-if="baseStationsLoading" class="loading-list">
|
|
<text class="loading-text">加载基站信息...</text>
|
|
</view>
|
|
|
|
<view v-else-if="hasBaseStations" class="base-stations-list">
|
|
<view v-for="(station, index) in baseStations" :key="station.id" class="base-station-item">
|
|
<view class="station-header">
|
|
<text class="station-name">{{ station.name }}</text>
|
|
<view class="station-status" :class="{ 'online': station.isOnline, 'offline': !station.isOnline }">
|
|
<text class="status-indicator-dot">●</text>
|
|
</view>
|
|
</view>
|
|
<text class="station-location">{{ station.location }}</text>
|
|
<view class="station-details">
|
|
<text class="detail-item">信号: {{ station.signalStrength }}dBm</text>
|
|
<text class="detail-item">范围: {{ station.range }}m</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<view v-else class="empty-list">
|
|
<text class="empty-text">暂无基站信息</text>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 围栏管理 -->
|
|
<view class="fences-section">
|
|
<view class="section-header">
|
|
<text class="section-title">围栏管理</text>
|
|
<button class="add-btn" @click="showCreateFenceDialog">
|
|
<text class="btn-text">+</text>
|
|
</button>
|
|
</view>
|
|
|
|
<view v-if="fencesLoading" class="loading-list">
|
|
<text class="loading-text">加载围栏信息...</text>
|
|
</view>
|
|
|
|
<view v-else-if="hasFences" class="fences-list">
|
|
<view v-for="(fence, index) in fences" :key="fence.id" class="fence-item">
|
|
<view class="fence-header">
|
|
<text class="fence-name">{{ fence.name }}</text>
|
|
<view class="fence-status" :class="{ 'active': fence.isActive, 'inactive': !fence.isActive }">
|
|
<text class="status-text">{{ fence.isActive ? '激活' : '未激活' }}</text>
|
|
</view>
|
|
</view>
|
|
<view class="fence-details">
|
|
<text class="detail-text">类型: {{ getFenceTypeText(fence.type) }}</text>
|
|
<text class="detail-text">事件: {{ getFenceEventText(fence.eventType) }}</text>
|
|
<text v-if="fence.type === 'circle' && fence.radius != null" class="detail-text">半径: {{ fence.radius }}米</text>
|
|
</view>
|
|
<view class="fence-actions">
|
|
<button class="action-btn edit" @click="editFence(fence)">
|
|
<text class="action-text">编辑</text>
|
|
</button>
|
|
<button class="action-btn delete" @click="deleteFence(fence.id)">
|
|
<text class="action-text">删除</text>
|
|
</button>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<view v-else class="empty-list">
|
|
<text class="empty-text">暂无围栏设置</text>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 围栏事件 -->
|
|
<view class="events-section">
|
|
<view class="section-header">
|
|
<text class="section-title">围栏事件</text> <button class="clear-btn" @click="clearAllEvents" v-if="hasFenceEvents">
|
|
<text class="small-btn-text">清空</text>
|
|
</button>
|
|
</view>
|
|
|
|
<view v-if="eventsLoading" class="loading-list">
|
|
<text class="loading-text">加载事件记录...</text>
|
|
</view>
|
|
|
|
<view v-else-if="hasFenceEvents" class="events-list">
|
|
<view v-for="(event, index) in fenceEvents" :key="event.id" class="event-item" :class="{ 'unread': !event.isRead }" @click="markEventRead(event)">
|
|
<view class="event-header">
|
|
<text class="event-fence">{{ event.fence.name }}</text>
|
|
<text class="event-time">{{ formatEventTime(event.timestamp) }}</text>
|
|
</view>
|
|
<view class="event-details">
|
|
<text class="event-type" :class="{ 'enter': event.eventType === 'enter', 'exit': event.eventType === 'exit' }">
|
|
{{ event.eventType === 'enter' ? '进入' : '离开' }}
|
|
</text>
|
|
<text class="event-location">{{ formatLocation(event.location) }}</text>
|
|
</view>
|
|
<view v-if="!event.isRead" class="unread-indicator">
|
|
<text class="unread-dot">●</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<view v-else class="empty-list">
|
|
<text class="empty-text">暂无围栏事件</text>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 位置历史 -->
|
|
<view class="history-section">
|
|
<view class="section-header">
|
|
<text class="section-title">位置历史</text>
|
|
<button class="view-more-btn" @click="viewFullHistory">
|
|
<text class="small-btn-text">查看更多</text>
|
|
</button>
|
|
</view>
|
|
|
|
<view v-if="historyLoading" class="loading-list">
|
|
<text class="loading-text">加载历史记录...</text>
|
|
</view>
|
|
|
|
<view v-else-if="hasLocationHistory" class="history-list">
|
|
<view v-for="(history, index) in locationHistory" :key="history.id" class="history-item">
|
|
<view class="history-header">
|
|
<text class="history-station">{{ history.baseStation.name }}</text>
|
|
<text class="history-time">{{ formatDateTime(history.timestamp) }}</text>
|
|
</view>
|
|
<text class="history-location">{{ history.baseStation.location }}</text>
|
|
<text class="history-duration">停留时长: {{ formatDuration(history.duration) }}</text>
|
|
</view>
|
|
</view>
|
|
|
|
<view v-else class="empty-list">
|
|
<text class="empty-text">暂无历史记录</text>
|
|
</view>
|
|
</view>
|
|
</scroll-view>
|
|
</template>
|
|
|
|
<script setup lang="uts">
|
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
|
import { getCurrentUserId } from '@/utils/store.uts'
|
|
import { LocationService } from '@/utils/locationService.uts'
|
|
import type { LocationInfo, BaseStationInfo, FenceInfo, FenceEvent, LocationHistoryItem, LocationCoordinate, EstimatedLocation } from '@/utils/locationService.uts'
|
|
|
|
// 页面状态
|
|
const locationLoading = ref<boolean>(false)
|
|
const baseStationsLoading = ref<boolean>(false)
|
|
const fencesLoading = ref<boolean>(false)
|
|
const eventsLoading = ref<boolean>(false)
|
|
const historyLoading = ref<boolean>(false)
|
|
|
|
// 数据状态
|
|
const currentLocation = ref<LocationInfo | null>(null)
|
|
const baseStations = ref<BaseStationInfo[]>([])
|
|
const fences = ref<FenceInfo[]>([])
|
|
const fenceEvents = ref<FenceEvent[]>([])
|
|
const locationHistory = ref<LocationHistoryItem[]>([])
|
|
// 当前设备ID
|
|
const deviceId = ref<string>('')
|
|
|
|
// 计算属性 - 用于数组长度检查
|
|
const hasBaseStations = computed((): boolean => {
|
|
return Array.isArray(baseStations.value) && baseStations.value.length > 0
|
|
})
|
|
|
|
const hasFences = computed((): boolean => {
|
|
return Array.isArray(fences.value) && fences.value.length > 0
|
|
})
|
|
|
|
const hasFenceEvents = computed((): boolean => {
|
|
return Array.isArray(fenceEvents.value) && fenceEvents.value.length > 0
|
|
})
|
|
|
|
const hasLocationHistory = computed((): boolean => {
|
|
return Array.isArray(locationHistory.value) && locationHistory.value.length > 0
|
|
})
|
|
|
|
// 获取当前位置
|
|
const refreshLocation = async () => {
|
|
if (deviceId.value === '') {
|
|
uni.showToast({
|
|
title: '设备ID获取失败',
|
|
icon: 'error'
|
|
})
|
|
return
|
|
}
|
|
|
|
locationLoading.value = true
|
|
try {
|
|
const result = await LocationService.getCurrentLocation(deviceId.value)
|
|
if (result.status === 200 && result.data != null) {
|
|
currentLocation.value = result.data as LocationInfo
|
|
} else {
|
|
uni.showToast({
|
|
title: result.error?.errMsg ?? '获取位置失败',
|
|
icon: 'error'
|
|
})
|
|
}
|
|
} catch (error) {
|
|
console.error('获取位置失败:', error)
|
|
uni.showToast({
|
|
title: '获取位置失败',
|
|
icon: 'error'
|
|
})
|
|
} finally {
|
|
locationLoading.value = false
|
|
}
|
|
}
|
|
|
|
// 刷新基站列表
|
|
const refreshBaseStations = async () => {
|
|
baseStationsLoading.value = true
|
|
try {
|
|
const result = await LocationService.getBaseStations()
|
|
if (result.status === 200 && result.data != null) {
|
|
baseStations.value = result.data as BaseStationInfo[]
|
|
} else {
|
|
uni.showToast({
|
|
title: result.error?.errMsg ?? '获取基站失败',
|
|
icon: 'error'
|
|
})
|
|
}
|
|
} catch (error) {
|
|
console.error('获取基站失败:', error)
|
|
} finally {
|
|
baseStationsLoading.value = false
|
|
}
|
|
}
|
|
|
|
// 加载围栏列表
|
|
const loadFences = async () => {
|
|
if (deviceId.value === '') return
|
|
|
|
fencesLoading.value = true
|
|
try {
|
|
const result = await LocationService.getFences(deviceId.value)
|
|
if (result.status === 200 && result.data != null) {
|
|
fences.value = result.data as FenceInfo[]
|
|
}
|
|
} catch (error) {
|
|
console.error('获取围栏失败:', error)
|
|
} finally {
|
|
fencesLoading.value = false
|
|
}
|
|
}
|
|
|
|
// 加载围栏事件
|
|
const loadFenceEvents = async () => {
|
|
if (deviceId.value === '') return
|
|
|
|
eventsLoading.value = true
|
|
try {
|
|
const result = await LocationService.getFenceEvents(deviceId.value, 10)
|
|
if (result.status === 200 && result.data != null) {
|
|
fenceEvents.value = result.data as FenceEvent[]
|
|
}
|
|
} catch (error) {
|
|
console.error('获取围栏事件失败:', error)
|
|
} finally {
|
|
eventsLoading.value = false
|
|
}
|
|
}
|
|
|
|
// 加载位置历史
|
|
const loadLocationHistory = async () => {
|
|
if (deviceId.value === '') return
|
|
|
|
historyLoading.value = true
|
|
try {
|
|
const endDate = new Date().toISOString()
|
|
const startDate = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString() // 7天前
|
|
const result = await LocationService.getLocationHistory(deviceId.value, startDate, endDate)
|
|
if (result.status === 200 && result.data != null) {
|
|
locationHistory.value = (result.data as LocationHistoryItem[]).slice(0, 5) // 只显示最近5条
|
|
}
|
|
} catch (error) {
|
|
console.error('获取位置历史失败:', error)
|
|
} finally {
|
|
historyLoading.value = false
|
|
}
|
|
}
|
|
|
|
// 显示创建围栏对话框
|
|
const showCreateFenceDialog = () => {
|
|
uni.showModal({
|
|
title: '创建围栏',
|
|
content: '围栏创建功能开发中,敬请期待',
|
|
showCancel: false
|
|
})
|
|
}
|
|
|
|
// 编辑围栏
|
|
const editFence = (fence: FenceInfo) => {
|
|
uni.showModal({
|
|
title: '编辑围栏',
|
|
content: `编辑围栏"${fence.name}"功能开发中`,
|
|
showCancel: false
|
|
})
|
|
}
|
|
|
|
// 删除围栏
|
|
const deleteFence = async (fenceId: string) => {
|
|
const result = await new Promise<boolean>((resolve) => {
|
|
uni.showModal({
|
|
title: '确认删除',
|
|
content: '确定要删除这个围栏吗?',
|
|
success: (res) => {
|
|
resolve(res.confirm)
|
|
}
|
|
})
|
|
})
|
|
|
|
if (!result) return
|
|
|
|
try {
|
|
const deleteResult = await LocationService.deleteFence(fenceId)
|
|
if (deleteResult.status === 200) {
|
|
uni.showToast({
|
|
title: '删除成功',
|
|
icon: 'success'
|
|
})
|
|
// 重新加载围栏列表
|
|
await loadFences()
|
|
} else {
|
|
uni.showToast({
|
|
title: deleteResult.error?.errMsg ?? '删除失败',
|
|
icon: 'error'
|
|
})
|
|
}
|
|
} catch (error) {
|
|
console.error('删除围栏失败:', error)
|
|
uni.showToast({
|
|
title: '删除失败',
|
|
icon: 'error'
|
|
})
|
|
}
|
|
}
|
|
|
|
// 标记事件为已读
|
|
const markEventRead = async (event: FenceEvent) => {
|
|
if (event.isRead) return
|
|
|
|
try {
|
|
const result = await LocationService.markEventAsRead(event.id)
|
|
if (result.status === 200) {
|
|
// 更新本地状态
|
|
const index = fenceEvents.value.findIndex(e => e.id === event.id)
|
|
if (index >= 0) {
|
|
fenceEvents.value[index].isRead = true
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('标记事件失败:', error)
|
|
}
|
|
}
|
|
|
|
// 清空所有事件
|
|
const clearAllEvents = () => {
|
|
uni.showModal({
|
|
title: '确认清空',
|
|
content: '确定要清空所有围栏事件吗?',
|
|
success: (res) => {
|
|
if (res.confirm) {
|
|
fenceEvents.value = []
|
|
uni.showToast({
|
|
title: '已清空',
|
|
icon: 'success'
|
|
})
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// 查看完整历史
|
|
const viewFullHistory = () => {
|
|
uni.showModal({
|
|
title: '位置历史',
|
|
content: '完整历史记录页面开发中',
|
|
showCancel: false
|
|
})
|
|
}
|
|
// 工具函数
|
|
const getStatusText = (status: string | undefined): string => {
|
|
switch (status) {
|
|
case 'online': return '在线'
|
|
case 'offline': return '离线'
|
|
default: return '未知'
|
|
}
|
|
}
|
|
|
|
// 基站信息访问函数
|
|
const getBaseStationName = (baseStation: BaseStationInfo | null): string => {
|
|
|
|
return baseStation?.name ?? '--'
|
|
}
|
|
|
|
const getBaseStationLocation = (baseStation: BaseStationInfo | null): string => {
|
|
return baseStation?.location ?? '--'
|
|
}
|
|
|
|
const getBaseStationSignalStrength = (baseStation: BaseStationInfo | null): number | string => {
|
|
return baseStation != null ? baseStation.signalStrength : ''
|
|
}
|
|
|
|
const getBaseStationRange = (baseStation: BaseStationInfo | null): number | string => {
|
|
return baseStation != null ? baseStation.range : ''
|
|
}
|
|
|
|
// 估算位置访问函数
|
|
const getEstimatedLongitude = (estimatedLocation: EstimatedLocation | null): string => {
|
|
return estimatedLocation != null ? estimatedLocation.longitude.toFixed(6) : ''
|
|
}
|
|
|
|
const getEstimatedLatitude = (estimatedLocation: EstimatedLocation | null): string => {
|
|
return estimatedLocation != null ? estimatedLocation.latitude.toFixed(6) : ''
|
|
}
|
|
|
|
const getEstimatedAccuracy = (estimatedLocation: EstimatedLocation | null): string => {
|
|
return estimatedLocation != null ? estimatedLocation.accuracy.toString() : ''
|
|
}
|
|
|
|
const getFenceTypeText = (type: string): string => {
|
|
switch (type) {
|
|
case 'circle': return '圆形'
|
|
case 'polygon': return '多边形'
|
|
default: return '未知'
|
|
}
|
|
}
|
|
|
|
const getFenceEventText = (eventType: string): string => {
|
|
switch (eventType) {
|
|
case 'enter': return '进入提醒'
|
|
case 'exit': return '离开提醒'
|
|
case 'both': return '进入/离开提醒'
|
|
default: return '未知'
|
|
}
|
|
}
|
|
|
|
const formatDateTime = (dateStr: string | null): string => {
|
|
if (dateStr == null || dateStr == '') return '--'
|
|
|
|
try {
|
|
const date = new Date(dateStr)
|
|
return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
|
|
} catch {
|
|
return '--'
|
|
}
|
|
}
|
|
|
|
const formatEventTime = (dateStr: string): string => {
|
|
if (dateStr == null || dateStr == '') return '--'
|
|
|
|
try {
|
|
const date = new Date(dateStr)
|
|
const now = new Date()
|
|
const diff = now.getTime() - date.getTime()
|
|
|
|
if (diff < 60000) { // 1分钟内
|
|
return '刚刚'
|
|
} else if (diff < 3600000) { // 1小时内
|
|
const minutes = Math.floor(diff / 60000)
|
|
return `${minutes}分钟前`
|
|
} else if (diff < 86400000) { // 24小时内
|
|
const hours = Math.floor(diff / 3600000)
|
|
return `${hours}小时前`
|
|
} else {
|
|
const days = Math.floor(diff / 86400000)
|
|
return `${days}天前`
|
|
}
|
|
} catch {
|
|
return '--'
|
|
}
|
|
}
|
|
const formatLocation = (location: LocationCoordinate): string => {
|
|
return `${location.longitude.toFixed(4)}, ${location.latitude.toFixed(4)}`
|
|
}
|
|
|
|
const formatDuration = (seconds: number): string => {
|
|
if (seconds < 60) {
|
|
return `${seconds}秒`
|
|
} else if (seconds < 3600) {
|
|
const minutes = Math.floor(seconds / 60)
|
|
return `${minutes}分钟`
|
|
} else {
|
|
const hours = Math.floor(seconds / 3600)
|
|
const minutes = Math.floor((seconds % 3600) / 60)
|
|
return `${hours}小时${minutes > 0 ? minutes + '分钟' : ''}`
|
|
}
|
|
}
|
|
|
|
// 生命周期
|
|
onMounted(() => {
|
|
// 获取设备ID - 这里应该从用户信息或设备信息中获取
|
|
const userId = getCurrentUserId()
|
|
if (userId != null && userId !== '') {
|
|
deviceId.value = `device_${userId}` // 模拟设备ID
|
|
} else {
|
|
deviceId.value = 'device_demo' // 演示用设备ID
|
|
}
|
|
|
|
// 加载所有数据
|
|
refreshLocation()
|
|
refreshBaseStations()
|
|
loadFences()
|
|
loadFenceEvents()
|
|
loadLocationHistory()
|
|
})
|
|
</script>
|
|
|
|
<style>
|
|
.location-page {
|
|
display: flex;
|
|
flex: 1;
|
|
height: 100vh;
|
|
background-color: #f5f5f5;
|
|
padding: 32rpx;
|
|
padding-bottom: 40rpx;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.header {
|
|
margin-bottom: 32rpx;
|
|
}
|
|
|
|
.title {
|
|
font-size: 48rpx;
|
|
font-weight: bold;
|
|
color: #333333;
|
|
margin-bottom: 8rpx;
|
|
}
|
|
|
|
.subtitle {
|
|
font-size: 28rpx;
|
|
color: #666666;
|
|
}
|
|
|
|
/* 当前位置卡片 */
|
|
.current-location-section {
|
|
margin-bottom: 32rpx;
|
|
}
|
|
|
|
.location-card {
|
|
background: #ffffff;
|
|
border-radius: 16rpx;
|
|
padding: 32rpx;
|
|
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.card-header {
|
|
display: flex;
|
|
flex-direction: row;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 24rpx;
|
|
}
|
|
|
|
.card-title {
|
|
font-size: 32rpx;
|
|
font-weight: bold;
|
|
color: #333333;
|
|
}
|
|
|
|
.status-indicator {
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
}
|
|
|
|
.status-indicator.online .status-dot {
|
|
color: #34c759;
|
|
}
|
|
|
|
.status-indicator.offline .status-dot {
|
|
color: #ff3b30;
|
|
}
|
|
|
|
.status-dot {
|
|
font-size: 24rpx;
|
|
margin-right: 8rpx;
|
|
}
|
|
|
|
.status-text {
|
|
font-size: 24rpx;
|
|
color: #666666;
|
|
}
|
|
|
|
.loading-container,
|
|
.error-container {
|
|
padding: 40rpx 0;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
}
|
|
|
|
.loading-text,
|
|
.error-text {
|
|
font-size: 28rpx;
|
|
color: #999999;
|
|
}
|
|
|
|
.location-content {
|
|
margin-bottom: 24rpx;
|
|
}
|
|
|
|
.base-station-info {
|
|
margin-bottom: 24rpx;
|
|
}
|
|
|
|
.info-row {
|
|
display: flex;
|
|
flex-direction: row;
|
|
justify-content: space-between;
|
|
margin-bottom: 12rpx;
|
|
}
|
|
|
|
.info-label {
|
|
font-size: 26rpx;
|
|
color: #666666;
|
|
width: 120rpx;
|
|
}
|
|
|
|
.info-value {
|
|
font-size: 26rpx;
|
|
color: #333333;
|
|
flex: 1;
|
|
text-align: right;
|
|
}
|
|
|
|
.estimated-location {
|
|
margin-bottom: 24rpx;
|
|
}
|
|
|
|
.section-label {
|
|
font-size: 24rpx;
|
|
color: #666666;
|
|
margin-bottom: 12rpx;
|
|
}
|
|
|
|
.coordinates {
|
|
background: #f8f9fa;
|
|
border-radius: 8rpx;
|
|
padding: 16rpx;
|
|
}
|
|
|
|
.coordinate-text,
|
|
.accuracy-text {
|
|
font-size: 24rpx;
|
|
color: #333333;
|
|
margin-bottom: 4rpx;
|
|
}
|
|
|
|
.accuracy-text {
|
|
color: #666666;
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.update-info {
|
|
margin-bottom: 24rpx;
|
|
}
|
|
|
|
.update-text {
|
|
font-size: 22rpx;
|
|
color: #999999;
|
|
}
|
|
|
|
.refresh-btn {
|
|
width: 100%;
|
|
height: 80rpx;
|
|
background: linear-gradient(to right, #667eea, #764ba2);
|
|
border-radius: 12rpx;
|
|
border: none;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
}
|
|
|
|
.refresh-btn:disabled {
|
|
opacity: 0.6;
|
|
}
|
|
|
|
.btn-text {
|
|
font-size: 28rpx;
|
|
color: #ffffff;
|
|
font-weight: bold;
|
|
}
|
|
|
|
/* 通用列表样式 */
|
|
.section-header {
|
|
display: flex;
|
|
flex-direction: row;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 24rpx;
|
|
}
|
|
|
|
.section-title {
|
|
font-size: 32rpx;
|
|
font-weight: bold;
|
|
color: #333333;
|
|
}
|
|
|
|
.refresh-small-btn,
|
|
.add-btn,
|
|
.clear-btn,
|
|
.view-more-btn {
|
|
padding: 12rpx 24rpx;
|
|
background: #f0f0f0;
|
|
border-radius: 20rpx;
|
|
border: none;
|
|
}
|
|
|
|
.add-btn {
|
|
background: #007aff;
|
|
width: 60rpx;
|
|
height: 60rpx;
|
|
border-radius: 30rpx;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
}
|
|
|
|
.add-btn .btn-text {
|
|
font-size: 32rpx;
|
|
color: #ffffff;
|
|
}
|
|
|
|
.small-btn-text {
|
|
font-size: 24rpx;
|
|
color: #666666;
|
|
}
|
|
|
|
.loading-list,
|
|
.empty-list {
|
|
background: #ffffff;
|
|
border-radius: 12rpx;
|
|
padding: 40rpx;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
}
|
|
|
|
.empty-text {
|
|
font-size: 26rpx;
|
|
color: #999999;
|
|
}
|
|
|
|
/* 基站列表样式 */
|
|
.base-stations-section {
|
|
margin-bottom: 32rpx;
|
|
}
|
|
|
|
.base-stations-list {
|
|
background: #ffffff;
|
|
border-radius: 12rpx;
|
|
padding: 16rpx;
|
|
}
|
|
|
|
.base-station-item {
|
|
padding: 20rpx;
|
|
border-bottom: 1rpx solid #f0f0f0;
|
|
}
|
|
|
|
.base-station-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.station-header {
|
|
display: flex;
|
|
flex-direction: row;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 8rpx;
|
|
}
|
|
|
|
.station-name {
|
|
font-size: 28rpx;
|
|
font-weight: bold;
|
|
color: #333333;
|
|
}
|
|
|
|
.station-status.online .status-indicator-dot {
|
|
color: #34c759;
|
|
}
|
|
|
|
.station-status.offline .status-indicator-dot {
|
|
color: #ff3b30;
|
|
}
|
|
|
|
.status-indicator-dot {
|
|
font-size: 20rpx;
|
|
}
|
|
|
|
.station-location {
|
|
font-size: 24rpx;
|
|
color: #666666;
|
|
margin-bottom: 8rpx;
|
|
}
|
|
|
|
.station-details {
|
|
display: flex;
|
|
flex-direction: row;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.detail-item {
|
|
font-size: 22rpx;
|
|
color: #999999;
|
|
}
|
|
|
|
/* 围栏列表样式 */
|
|
.fences-section {
|
|
margin-bottom: 32rpx;
|
|
}
|
|
|
|
.fences-list {
|
|
background: #ffffff;
|
|
border-radius: 12rpx;
|
|
padding: 16rpx;
|
|
}
|
|
|
|
.fence-item {
|
|
padding: 20rpx;
|
|
border-bottom: 1rpx solid #f0f0f0;
|
|
}
|
|
|
|
.fence-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.fence-header {
|
|
display: flex;
|
|
flex-direction: row;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 12rpx;
|
|
}
|
|
|
|
.fence-name {
|
|
font-size: 28rpx;
|
|
font-weight: bold;
|
|
color: #333333;
|
|
}
|
|
|
|
.fence-status {
|
|
padding: 6rpx 12rpx;
|
|
border-radius: 12rpx;
|
|
font-size: 20rpx;
|
|
}
|
|
|
|
.fence-status.active {
|
|
background: #d4edda;
|
|
color: #155724;
|
|
}
|
|
|
|
.fence-status.inactive {
|
|
background: #f8d7da;
|
|
color: #721c24;
|
|
}
|
|
|
|
.fence-details {
|
|
margin-bottom: 16rpx;
|
|
}
|
|
|
|
.detail-text {
|
|
font-size: 24rpx;
|
|
color: #666666;
|
|
margin-bottom: 4rpx;
|
|
}
|
|
|
|
.fence-actions {
|
|
display: flex;
|
|
flex-direction: row;
|
|
justify-content: flex-end;
|
|
}
|
|
|
|
.action-btn {
|
|
padding: 8rpx 16rpx;
|
|
border-radius: 8rpx;
|
|
border: none;
|
|
margin-left: 12rpx;
|
|
}
|
|
|
|
.action-btn.edit {
|
|
background: #007aff;
|
|
}
|
|
|
|
.action-btn.delete {
|
|
background: #ff3b30;
|
|
}
|
|
|
|
.action-text {
|
|
font-size: 22rpx;
|
|
color: #ffffff;
|
|
}
|
|
|
|
/* 围栏事件样式 */
|
|
.events-section {
|
|
margin-bottom: 32rpx;
|
|
}
|
|
|
|
.events-list {
|
|
background: #ffffff;
|
|
border-radius: 12rpx;
|
|
padding: 16rpx;
|
|
}
|
|
|
|
.event-item {
|
|
padding: 20rpx;
|
|
border-bottom: 1rpx solid #f0f0f0;
|
|
position: relative;
|
|
}
|
|
|
|
.event-item.unread {
|
|
background: #f8f9ff;
|
|
border-left: 4rpx solid #007aff;
|
|
}
|
|
|
|
.event-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.event-header {
|
|
display: flex;
|
|
flex-direction: row;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 8rpx;
|
|
}
|
|
|
|
.event-fence {
|
|
font-size: 26rpx;
|
|
font-weight: bold;
|
|
color: #333333;
|
|
}
|
|
|
|
.event-time {
|
|
font-size: 22rpx;
|
|
color: #999999;
|
|
}
|
|
|
|
.event-details {
|
|
display: flex;
|
|
flex-direction: row;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.event-type {
|
|
font-size: 24rpx;
|
|
padding: 4rpx 8rpx;
|
|
border-radius: 8rpx;
|
|
}
|
|
|
|
.event-type.enter {
|
|
background: #d4edda;
|
|
color: #155724;
|
|
}
|
|
|
|
.event-type.exit {
|
|
background: #fff3cd;
|
|
color: #856404;
|
|
}
|
|
|
|
.event-location {
|
|
font-size: 22rpx;
|
|
color: #666666;
|
|
}
|
|
|
|
.unread-indicator {
|
|
position: absolute;
|
|
top: 20rpx;
|
|
right: 20rpx;
|
|
}
|
|
|
|
.unread-dot {
|
|
font-size: 16rpx;
|
|
color: #007aff;
|
|
}
|
|
|
|
/* 位置历史样式 */
|
|
.history-section {
|
|
margin-bottom: 32rpx;
|
|
}
|
|
|
|
.history-list {
|
|
background: #ffffff;
|
|
border-radius: 12rpx;
|
|
padding: 16rpx;
|
|
}
|
|
|
|
.history-item {
|
|
padding: 20rpx;
|
|
border-bottom: 1rpx solid #f0f0f0;
|
|
}
|
|
|
|
.history-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.history-header {
|
|
display: flex;
|
|
flex-direction: row;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 8rpx;
|
|
}
|
|
|
|
.history-station {
|
|
font-size: 26rpx;
|
|
font-weight: bold;
|
|
color: #333333;
|
|
}
|
|
|
|
.history-time {
|
|
font-size: 22rpx;
|
|
color: #999999;
|
|
}
|
|
|
|
.history-location {
|
|
font-size: 24rpx;
|
|
color: #666666;
|
|
margin-bottom: 4rpx;
|
|
}
|
|
|
|
.history-duration {
|
|
font-size: 22rpx;
|
|
color: #999999;
|
|
}
|
|
</style>
|