TrackView.vue 8.39 KB
<template>
  <div class="track-container">
    <!-- 搜索条件 -->
    <el-card shadow="never" class="search-card">
      <el-form :inline="true" :model="searchForm" size="small">
        <el-form-item label="选择人员">
          <el-select v-model="searchForm.userId" placeholder="请选择人员" filterable clearable
                     @change="handleSearch">
            <el-option v-for="user in userList" :key="user.userId"
                       :label="user.userName + (user.tel ? ' - ' + user.tel : '')"
                       :value="user.userId" />
          </el-select>
        </el-form-item>
        <el-form-item label="开始时间">
          <el-date-picker v-model="searchForm.startTime" type="datetime"
                          placeholder="选择开始时间" format="yyyy-MM-dd HH:mm:ss"
                          value-format="yyyy-MM-dd HH:mm:ss" @change="handleSearch" />
        </el-form-item>
        <el-form-item label="结束时间">
          <el-date-picker v-model="searchForm.endTime" type="datetime"
                          placeholder="选择结束时间" format="yyyy-MM-dd HH:mm:ss"
                          value-format="yyyy-MM-dd HH:mm:ss" @change="handleSearch" />
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="handleSearch" icon="el-icon-search">查询</el-button>
          <el-button @click="handlePlayTrack" icon="el-icon-video-play" :disabled="!trackData.length">
            播放轨迹
          </el-button>
        </el-form-item>
      </el-form>
    </el-card>

    <!-- 地图容器(独立 div,避免 el-card 干扰尺寸测量) -->
    <div id="amap-container" ref="mapContainer" class="map-container"></div>

    <!-- 轨迹点列表 -->
    <el-card shadow="never" class="list-card">
      <div slot="header">
        <span>轨迹点列表(共 {{ trackData.length }} 个点)</span>
      </div>
      <el-table :data="trackData" border stripe size="small" max-height="300">
        <el-table-column prop="report_time" label="上报时间" width="180" />
        <el-table-column prop="longitude" label="经度" width="140" />
        <el-table-column prop="latitude" label="纬度" width="140" />
        <el-table-column prop="loc_status" label="状态" width="100">
          <template slot-scope="scope">
            <el-tag :type="scope.row.loc_status === 'NORMAL' ? 'success' : 'danger'" size="small">
              {{ scope.row.loc_status === 'NORMAL' ? '正常' : '异常' }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="操作" width="100">
          <template slot-scope="scope">
            <el-button type="text" size="small" @click="locatePoint(scope.row)">定位</el-button>
          </template>
        </el-table-column>
      </el-table>
    </el-card>
  </div>
</template>

<script>
import { queryLocationTracks } from '@/api/property/propertyApi'
import { listSystemUsers } from '@/api/staff/systemUserApi'

export default {
  name: 'TrackView',
  data() {
    return {
      searchForm: { userId: '', startTime: '', endTime: '' },
      userList: [],
      trackData: [],
      map: null,
      polyline: null,
      markers: [],
      movingMarker: null,
      playing: false,
      playTimer: null,
      playIndex: 0
    }
  },
  mounted() {
    this.loadUserList()
    this.initMap()
  },
  beforeDestroy() {
    if (this.playTimer) clearInterval(this.playTimer)
    if (this.map) {
      this.map.destroy()
      this.map = null
    }
  },
  methods: {
    async loadUserList() {
      try {
        const { data } = await listSystemUsers({ page: 1, row: 200 })
        this.userList = data || []
      } catch (e) {
        console.error('加载人员列表失败', e)
      }
    },

    initMap() {
      // 确保 DOM 已渲染且 AMap 已加载
      this.$nextTick(() => {
        const tryCreate = () => {
          if (!window.AMap) {
            setTimeout(tryCreate, 200)
            return
          }
          const container = this.$refs.mapContainer
          if (!container || container.offsetWidth === 0) {
            setTimeout(tryCreate, 200)
            return
          }
          this.createMap(container)
        }
        tryCreate()
      })
    },

    createMap(container) {
      if (this.map) return
      this.map = new window.AMap.Map(container, {
        zoom: 13,
        center: [116.397428, 39.90923],
        resizeEnable: true
      })
    },

    async handleSearch() {
      if (!this.searchForm.userId) {
        this.$message.warning('请选择人员')
        return
      }
      try {
        const params = { userId: this.searchForm.userId }
        if (this.searchForm.startTime) params.startTime = this.searchForm.startTime
        if (this.searchForm.endTime) params.endTime = this.searchForm.endTime
        const res = await queryLocationTracks(params)
        const list = Array.isArray(res.data) ? res.data : (res.data && res.data.data) || []
        this.trackData = list
        this.$nextTick(() => this.renderTrack())
      } catch (e) {
        this.$message.error('查询轨迹失败')
        console.error(e)
      }
    },

    renderTrack() {
      if (!this.map || !this.trackData.length) return
      this.clearMapElements()

      const path = this.trackData.map(p => [p.longitude, p.latitude])

      this.polyline = new window.AMap.Polyline({
        map: this.map,
        path: path,
        strokeColor: '#1890ff',
        strokeWeight: 4,
        strokeOpacity: 0.8
      })

      if (path.length > 0) {
        const start = path[0]
        const end = path[path.length - 1]
        const startMarker = new window.AMap.Marker({
          map: this.map,
          position: start,
          title: '起点',
          icon: new window.AMap.Icon({
            size: new window.AMap.Size(25, 34),
            imageSize: new window.AMap.Size(25, 34),
            image: 'https://webapi.amap.com/theme/v1.3/markers/n/start.png'
          })
        })
        const endMarker = new window.AMap.Marker({
          map: this.map,
          position: end,
          title: '终点',
          icon: new window.AMap.Icon({
            size: new window.AMap.Size(25, 34),
            imageSize: new window.AMap.Size(25, 34),
            image: 'https://webapi.amap.com/theme/v1.3/markers/n/end.png'
          })
        })
        this.markers.push(startMarker, endMarker)
      }
      this.map.setFitView(null, false, [60, 60, 60, 60])
    },

    clearMapElements() {
      if (this.polyline) { this.polyline.setMap(null); this.polyline = null }
      this.markers.forEach(m => m.setMap(null))
      this.markers = []
      if (this.movingMarker) { this.movingMarker.setMap(null); this.movingMarker = null }
    },

    handlePlayTrack() {
      if (this.playing) {
        clearInterval(this.playTimer)
        this.playTimer = null
        this.playing = false
        this.$message.info('轨迹播放已暂停')
        return
      }
      if (!this.trackData.length) return
      this.playing = true
      this.playIndex = 0

      if (this.movingMarker) { this.movingMarker.setMap(null); this.movingMarker = null }
      const firstPoint = this.trackData[0]
      this.movingMarker = new window.AMap.Marker({
        map: this.map,
        position: [firstPoint.longitude, firstPoint.latitude],
        icon: new window.AMap.Icon({
          size: new window.AMap.Size(30, 30),
          imageSize: new window.AMap.Size(30, 30),
          image: 'https://webapi.amap.com/theme/v1.3/markers/n/mark_r.png'
        })
      })

      this.playTimer = setInterval(() => {
        if (this.playIndex >= this.trackData.length) {
          clearInterval(this.playTimer)
          this.playTimer = null
          this.playing = false
          this.$message.success('轨迹播放完成')
          return
        }
        const point = this.trackData[this.playIndex]
        if (this.movingMarker) {
          this.movingMarker.setPosition([point.longitude, point.latitude])
        }
        this.map.setCenter([point.longitude, point.latitude])
        this.playIndex++
      }, 500)
    },

    locatePoint(row) {
      if (this.map) {
        this.map.setCenter([row.longitude, row.latitude])
        this.map.setZoom(18)
      }
    }
  }
}
</script>

<style scoped>
.track-container { padding: 16px; }
.search-card { margin-bottom: 16px; }
.map-container {
  width: 100%;
  height: 500px;
  margin-bottom: 16px;
  border-radius: 4px;
  border: 1px solid #ebeef5;
}
.list-card { margin-bottom: 16px; }
</style>