Files
akmon/pages/sport/student/location.uvue
2026-01-20 08:04:15 +08:00

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>