|
@@ -11,32 +11,84 @@
|
|
|
{{ this.$t('watch.getNewPlace') }}</el-button>
|
|
|
<el-tag v-if="myTitle" style="margin-left: 20px;" type="warning">{{ myTitle }}</el-tag>
|
|
|
</div>
|
|
|
- <div>
|
|
|
- <el-row>
|
|
|
- <el-col :span="6" v-for="(item, index) in beaconDevices" :key="index">
|
|
|
- <el-card :id="'myFrame'+item.id" shadow="never" class="myClass">
|
|
|
- <div :style="item.device_type === getDeviceList().ELECTRONIC_FENCE ? 'color: red' : ''">
|
|
|
- <svg-icon :id="'myIcon' + item.id" icon-class="footmark" class-name="footmark" style="font-size: 30px;display: none" />
|
|
|
- <svg-icon icon-class="sickroom" style="font-size: 40px;padding-right: 20px" />{{ item.name }}
|
|
|
+ <div class="location-map">
|
|
|
+ <div class="grid-background"></div>
|
|
|
+
|
|
|
+ <div class="beacon-grid">
|
|
|
+ <el-card
|
|
|
+ v-for="item in beaconDevices"
|
|
|
+ :key="item.id"
|
|
|
+ :id="'myFrame'+item.id"
|
|
|
+ class="beacon-point"
|
|
|
+ :class="{
|
|
|
+ 'active': isBeaconActive(item.id),
|
|
|
+ 'fence': item.device_type === 39
|
|
|
+ }"
|
|
|
+ :style="getBeaconStyle(item)">
|
|
|
+ <div :class="['beacon-content', {
|
|
|
+ 'active': isBeaconActive(item.id),
|
|
|
+ 'fence': item.device_type === 39
|
|
|
+ }]">
|
|
|
+ <div class="beacon-icon">
|
|
|
+ <div :class="['radar-wave', {
|
|
|
+ 'fence-wave': item.device_type === 39
|
|
|
+ }]" v-if="isBeaconActive(item.id)"></div>
|
|
|
+ <svg-icon
|
|
|
+ :id="'myIcon' + item.id"
|
|
|
+ icon-class="footmark"
|
|
|
+ class="footmark-icon"
|
|
|
+ :style="{
|
|
|
+ display: isBeaconActive(item.id) ? 'inline-block' : 'none',
|
|
|
+ color: item.device_type === 39 ? '#fff' : ''
|
|
|
+ }" />
|
|
|
</div>
|
|
|
- <div style="float:right">
|
|
|
- <span :id="'myText' + item.id" ></span>
|
|
|
+ <div class="beacon-name">{{ item.name }}</div>
|
|
|
+ <div :id="'myText' + item.id" class="sequence-number"></div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <svg class="path-overlay" :viewBox="viewBox">
|
|
|
+ <path
|
|
|
+ :d="pathData"
|
|
|
+ class="movement-path-bg"
|
|
|
+ stroke="#E1E1E1"
|
|
|
+ stroke-width="3"
|
|
|
+ stroke-dasharray="5,5"
|
|
|
+ fill="none" />
|
|
|
+ <path
|
|
|
+ :d="pathData"
|
|
|
+ class="movement-path"
|
|
|
+ fill="none" />
|
|
|
+ <circle
|
|
|
+ v-if="pathData"
|
|
|
+ class="moving-dot"
|
|
|
+ r="6"
|
|
|
+ fill="#409EFF">
|
|
|
+ <animateMotion
|
|
|
+ :path="pathData"
|
|
|
+ dur="3s"
|
|
|
+ repeatCount="indefinite" />
|
|
|
+ </circle>
|
|
|
+ </svg>
|
|
|
+ </div>
|
|
|
+ <div v-if="locationList.length > 0" class="timeline-container">
|
|
|
+ <el-timeline>
|
|
|
+ <el-timeline-item
|
|
|
+ v-for="(item, index) in locationList"
|
|
|
+ :key="index"
|
|
|
+ :timestamp="formatterCreateTime(item.create_time)"
|
|
|
+ :type="getTimelineItemType(index)">
|
|
|
+ <el-card class="timeline-card" :class="{'current-location': index === 0}">
|
|
|
+ <div class="location-info">
|
|
|
+ <i class="el-icon-location-outline"></i>
|
|
|
+ <span>{{ item.full_name }}</span>
|
|
|
+ <span v-if="index === 0" class="current-badge">当前位置</span>
|
|
|
</div>
|
|
|
-
|
|
|
</el-card>
|
|
|
- </el-col>
|
|
|
- </el-row>
|
|
|
- </div>
|
|
|
- <div v-if="locationList.length > 0" style="margin-top: 20px;">
|
|
|
- <el-card>
|
|
|
- <div v-for="(item, index) in locationList" :key="index" class="text item">
|
|
|
- <i class="el-icon-user-solid"></i> {{ $t('watch.you') }} <el-tag><i class="el-icon-time"></i>{{ formatterCreateTime(item.create_time )}}</el-tag> 在
|
|
|
- <el-tag type="success"><i class="el-icon-location-outline"></i>{{ item.full_name }}</el-tag>
|
|
|
- {{ $t('watch.nearby') }}
|
|
|
- </div>
|
|
|
- </el-card>
|
|
|
+ </el-timeline-item>
|
|
|
+ </el-timeline>
|
|
|
</div>
|
|
|
- <!--翻页-->
|
|
|
<el-pagination
|
|
|
v-if="pageData"
|
|
|
slot="pagination"
|
|
@@ -90,7 +142,10 @@ export default {
|
|
|
queryTime: null,
|
|
|
websock: null,
|
|
|
fullscreenLoading: false,
|
|
|
- myTitle: ''
|
|
|
+ myTitle: '',
|
|
|
+ pathData: '',
|
|
|
+ viewBox: '0 0 1000 1000',
|
|
|
+ gridPositions: null
|
|
|
}
|
|
|
},
|
|
|
watch: {
|
|
@@ -120,6 +175,7 @@ export default {
|
|
|
const _this = this
|
|
|
getLocationDeviceList(this.$store.getters.partId).then(res => {
|
|
|
_this.beaconDevices = res
|
|
|
+ _this.initGridPositions()
|
|
|
_this.API_GetList()
|
|
|
})
|
|
|
},
|
|
@@ -147,12 +203,31 @@ export default {
|
|
|
}
|
|
|
this.tableData.reverse()
|
|
|
for (let i = 0; i< _this.tableData.length; i++) {
|
|
|
- (function (t, data) { // 注意这里是形参
|
|
|
+ (function (t, data) {
|
|
|
setTimeout(function () {
|
|
|
if (myId) {
|
|
|
- document.getElementById(myId).style = 'background: #d3dce6'
|
|
|
+ const prevElement = document.getElementById(myId)
|
|
|
+ if (prevElement) {
|
|
|
+ const prevBeacon = _this.beaconDevices.find(b => b.id === parseInt(myId.replace('myFrame', '')))
|
|
|
+ if (prevBeacon && prevBeacon.device_type === 39) {
|
|
|
+ prevElement.style.background = '#F56C6C'
|
|
|
+ } else {
|
|
|
+ prevElement.style.background = '#d3dce6'
|
|
|
+ }
|
|
|
+ prevElement.style.color = prevBeacon?.device_type === 39 ? 'white' : '#333'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ const currentElement = document.getElementById('myFrame'+data.beacon_device_id)
|
|
|
+ if (currentElement) {
|
|
|
+ const beacon = _this.beaconDevices.find(b => b.id === data.beacon_device_id)
|
|
|
+ if (beacon && beacon.device_type === 39) {
|
|
|
+ currentElement.style.background = '#F56C6C'
|
|
|
+ currentElement.style.color = 'white'
|
|
|
+ } else {
|
|
|
+ currentElement.style.background = '#3DCB0A'
|
|
|
+ currentElement.style.color = 'white'
|
|
|
+ }
|
|
|
}
|
|
|
- document.getElementById('myFrame'+data.beacon_device_id).style = 'color: white;background: #3DCB0A'
|
|
|
document.getElementById('myIcon'+data.beacon_device_id).style.display='inline'
|
|
|
_this.$notify({
|
|
|
title: data.full_name,
|
|
@@ -169,14 +244,13 @@ export default {
|
|
|
} else {
|
|
|
text.innerText = (i+1)
|
|
|
}
|
|
|
- // document.getElementById('myText'+data.beacon_device_id).innerText = i+1+1
|
|
|
if (_this.clearList.find(p => p === data.beacon_device_id) == null) {
|
|
|
_this.clearList.push(data.beacon_device_id)
|
|
|
}
|
|
|
-
|
|
|
- }, 250 * t); // 还是每秒执行一次,不是累加的
|
|
|
- })(i, _this.tableData[i]) // 注意这里是实参,这里把要用的参数传进去
|
|
|
+ }, 250 * t)
|
|
|
+ })(i, _this.tableData[i])
|
|
|
}
|
|
|
+ this.updateMovementPath()
|
|
|
},
|
|
|
sx() {
|
|
|
this.queryTime = null
|
|
@@ -207,7 +281,14 @@ export default {
|
|
|
},
|
|
|
clearStyle() {
|
|
|
this.clearList.forEach(item => {
|
|
|
- document.getElementById('myFrame'+item).style = ''
|
|
|
+ const element = document.getElementById('myFrame'+item)
|
|
|
+ const beacon = this.beaconDevices.find(b => b.id === item)
|
|
|
+ if (beacon && beacon.device_type === 39) {
|
|
|
+ element.style.background = '#F56C6C'
|
|
|
+ element.style.color = 'white'
|
|
|
+ } else {
|
|
|
+ element.style = ''
|
|
|
+ }
|
|
|
document.getElementById('myIcon'+item).style.display='none'
|
|
|
document.getElementById('myText'+item).innerText = ''
|
|
|
})
|
|
@@ -275,27 +356,239 @@ export default {
|
|
|
},
|
|
|
getDeviceList() {
|
|
|
return DEVICE_TYPE;
|
|
|
+ },
|
|
|
+ initGridPositions() {
|
|
|
+ const totalBeacons = this.beaconDevices.length
|
|
|
+ const cols = Math.ceil(Math.sqrt(totalBeacons))
|
|
|
+ this.gridPositions = this.beaconDevices.reduce((acc, device, index) => {
|
|
|
+ acc[device.id] = {
|
|
|
+ x: (index % cols) + 1,
|
|
|
+ y: Math.floor(index / cols) + 1
|
|
|
+ }
|
|
|
+ return acc
|
|
|
+ }, {})
|
|
|
+ },
|
|
|
+ getBeaconStyle(beacon) {
|
|
|
+ if (!this.gridPositions) this.initGridPositions()
|
|
|
+ const pos = this.gridPositions[beacon.id]
|
|
|
+ return {
|
|
|
+ gridColumn: `${pos.x} / span 1`,
|
|
|
+ gridRow: `${pos.y} / span 1`
|
|
|
+ }
|
|
|
+ },
|
|
|
+ isBeaconActive(beaconId) {
|
|
|
+ return this.locationList.length > 0 &&
|
|
|
+ this.locationList[0].beacon_device_id === beaconId
|
|
|
+ },
|
|
|
+ updateMovementPath() {
|
|
|
+ if (this.locationList.length < 2 || !this.gridPositions) return
|
|
|
+
|
|
|
+ const points = this.locationList.map(location => {
|
|
|
+ const pos = this.gridPositions[location.beacon_device_id]
|
|
|
+ return `${pos.x * 100},${pos.y * 100}`
|
|
|
+ })
|
|
|
+
|
|
|
+ this.pathData = `M ${points.join(' L ')}`
|
|
|
+ },
|
|
|
+ getTimelineItemType(index) {
|
|
|
+ if (index === 0) return 'success'
|
|
|
+ if (index === 1) return 'primary'
|
|
|
+ return ''
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
</script>
|
|
|
|
|
|
-<style scoped type="text/scss">
|
|
|
-.el-row {
|
|
|
- margin-bottom: 20px;
|
|
|
-&:last-child {
|
|
|
- margin-bottom: 0;
|
|
|
- }
|
|
|
-}
|
|
|
-.el-col {
|
|
|
- border-radius: 30px;
|
|
|
+<style lang="scss" scoped>
|
|
|
+.location-map {
|
|
|
+ position: relative;
|
|
|
+ margin: 20px 0;
|
|
|
+ height: 600px;
|
|
|
+ background: #f8fafc;
|
|
|
+ border-radius: 8px;
|
|
|
padding: 20px;
|
|
|
+ overflow: hidden;
|
|
|
}
|
|
|
-.text {
|
|
|
- font-size: 14px;
|
|
|
+
|
|
|
+.grid-background {
|
|
|
+ position: absolute;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ bottom: 0;
|
|
|
+ background-image:
|
|
|
+ linear-gradient(rgba(200, 200, 200, 0.1) 1px, transparent 1px),
|
|
|
+ linear-gradient(90deg, rgba(200, 200, 200, 0.1) 1px, transparent 1px);
|
|
|
+ background-size: 20px 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.beacon-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
|
+ gap: 20px;
|
|
|
+ height: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.beacon-point {
|
|
|
+ position: relative;
|
|
|
+ transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
|
|
+ background: white;
|
|
|
+ border: none;
|
|
|
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ transform: translateY(-2px);
|
|
|
+ box-shadow: 0 8px 15px -3px rgba(0, 0, 0, 0.1);
|
|
|
+ }
|
|
|
+
|
|
|
+ &.active {
|
|
|
+ transform: scale(1.05);
|
|
|
+ background: #3DCB0A;
|
|
|
+ box-shadow: 0 0 25px rgba(61, 203, 10, 0.4);
|
|
|
+
|
|
|
+ .beacon-content {
|
|
|
+ color: white;
|
|
|
+
|
|
|
+ .sequence-number {
|
|
|
+ color: rgba(255, 255, 255, 0.8);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ &.fence {
|
|
|
+ background: #F56C6C !important;
|
|
|
+
|
|
|
+ .beacon-content {
|
|
|
+ color: white !important;
|
|
|
+ }
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ box-shadow: 0 8px 15px -3px rgba(245, 108, 108, 0.4);
|
|
|
+ }
|
|
|
+
|
|
|
+ &.active {
|
|
|
+ box-shadow: 0 0 25px rgba(245, 108, 108, 0.6);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.beacon-content {
|
|
|
+ text-align: center;
|
|
|
+ padding: 10px;
|
|
|
+
|
|
|
+ &.active {
|
|
|
+ color: white;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.fence {
|
|
|
+ color: white;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
-.item {
|
|
|
- margin-bottom: 18px;
|
|
|
+.beacon-name {
|
|
|
+ margin: 8px 0;
|
|
|
+ font-weight: bold;
|
|
|
+}
|
|
|
+
|
|
|
+.beacon-icon {
|
|
|
+ position: relative;
|
|
|
+ width: 48px;
|
|
|
+ height: 48px;
|
|
|
+ margin: 0 auto 12px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+}
|
|
|
+
|
|
|
+.radar-wave {
|
|
|
+ position: absolute;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: rgba(61, 203, 10, 0.2);
|
|
|
+ animation: radar-pulse 2s infinite;
|
|
|
+
|
|
|
+ &.fence-wave {
|
|
|
+ background: rgba(245, 108, 108, 0.2);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.footmark-icon {
|
|
|
+ font-size: 24px;
|
|
|
+ margin-bottom: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.sequence-number {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #666;
|
|
|
+}
|
|
|
+
|
|
|
+.path-overlay {
|
|
|
+ position: absolute;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ pointer-events: none;
|
|
|
+}
|
|
|
+
|
|
|
+.movement-path {
|
|
|
+ stroke: #409EFF;
|
|
|
+ stroke-width: 3;
|
|
|
+ stroke-linecap: round;
|
|
|
+ animation: drawPath 2s ease-out forwards;
|
|
|
+}
|
|
|
+
|
|
|
+.moving-dot {
|
|
|
+ filter: drop-shadow(0 0 4px rgba(64, 158, 255, 0.6));
|
|
|
+}
|
|
|
+
|
|
|
+.timeline-container {
|
|
|
+ margin: 20px 0;
|
|
|
+ max-height: 300px;
|
|
|
+ overflow-y: auto;
|
|
|
+}
|
|
|
+
|
|
|
+.timeline-card {
|
|
|
+ &.current-location {
|
|
|
+ border-left: 4px solid #3DCB0A;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.location-info {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.current-badge {
|
|
|
+ background: #3DCB0A;
|
|
|
+ color: white;
|
|
|
+ padding: 2px 8px;
|
|
|
+ border-radius: 12px;
|
|
|
+ font-size: 12px;
|
|
|
+ margin-left: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes radar-pulse {
|
|
|
+ 0% {
|
|
|
+ transform: scale(0.5);
|
|
|
+ opacity: 0.8;
|
|
|
+ }
|
|
|
+ 100% {
|
|
|
+ transform: scale(2);
|
|
|
+ opacity: 0;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes drawPath {
|
|
|
+ from {
|
|
|
+ stroke-dasharray: 1000;
|
|
|
+ stroke-dashoffset: 1000;
|
|
|
+ }
|
|
|
+ to {
|
|
|
+ stroke-dasharray: 1000;
|
|
|
+ stroke-dashoffset: 0;
|
|
|
+ }
|
|
|
}
|
|
|
</style>
|