Compare commits
6 Commits
8e7aecb0ab
...
d2f2922152
Author | SHA1 | Date | |
---|---|---|---|
d2f2922152 | |||
fcaba878f5 | |||
7576060097 | |||
56b3b0f438 | |||
005bd0ef63 | |||
5ca920b185 |
|
@ -5,10 +5,10 @@ VUE_APP_TITLE = 共享电动车管理系统
|
|||
ENV = 'development'
|
||||
|
||||
# 共享电动车管理系统/开发环境
|
||||
# VUE_APP_BASE_API = 'https://dche.ccttiot.com/prod-api'
|
||||
VUE_APP_BASE_API = 'https://dche.ccttiot.com/prod-api'
|
||||
# VUE_APP_BASE_API = 'https://che.chuangtewl.com/prod-api'
|
||||
|
||||
VUE_APP_BASE_API = 'http://localhost:8088'
|
||||
# VUE_APP_BASE_API = 'http://192.168.2.74:8088'
|
||||
|
||||
# 路由懒加载
|
||||
VUE_CLI_BABEL_TRANSPILE_MODULES = true
|
||||
|
|
|
@ -51,7 +51,7 @@
|
|||
"js-cookie": "3.0.1",
|
||||
"jsencrypt": "3.0.0-rc.1",
|
||||
"nprogress": "0.2.0",
|
||||
"qrcode": "^1.5.3",
|
||||
"qrcode": "^1.5.4",
|
||||
"quill": "1.3.7",
|
||||
"react-native-vector-icons": "^10.1.0",
|
||||
"screenfull": "5.0.2",
|
||||
|
|
|
@ -140,6 +140,14 @@ export function gettrajectory(data){
|
|||
method: 'post'
|
||||
});
|
||||
}
|
||||
|
||||
export function getTrajectoryDetails(data){
|
||||
console.log(data, 'data');
|
||||
return request({
|
||||
url: '/appVerify/trajectoryDetails?sn=' + data.sn + '&startTime=' + data.startTime + '&endTime=' + data.endTime,
|
||||
method: 'post'
|
||||
});
|
||||
}
|
||||
// 根据sn查询设备
|
||||
export function getDeviceBySn(sn){
|
||||
return request({
|
||||
|
|
958
src/components/Map/location/LocationMapss.vue
Normal file
958
src/components/Map/location/LocationMapss.vue
Normal file
|
@ -0,0 +1,958 @@
|
|||
<template>
|
||||
<div class="place-search-map" :style="{ width: width, height: height }" v-loading="loading">
|
||||
<div id="container"></div>
|
||||
|
||||
<!-- 轨迹回放控制面板 -->
|
||||
<div class="input-card">
|
||||
<h4>轨迹回放控制</h4>
|
||||
<div class="input-item">
|
||||
<div class="time-range">
|
||||
<el-date-picker v-model="startTime" type="datetime" placeholder="开始时间"
|
||||
value-format="yyyy-MM-dd HH:mm:ss" @change="handleTimeChange" />
|
||||
<span class="separator">至</span>
|
||||
<el-date-picker v-model="endTime" type="datetime" placeholder="结束时间"
|
||||
value-format="yyyy-MM-dd HH:mm:ss" @change="handleTimeChange" />
|
||||
</div>
|
||||
|
||||
<!-- 快捷时间选择 -->
|
||||
<div class="quick-select">
|
||||
<el-button v-for="item in timeOptions" :key="item.value" size="small"
|
||||
:type="activeTime === item.value ? 'primary' : ''" @click="selectTime(item.value)">
|
||||
{{ item.label }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 播放控制 -->
|
||||
<div class="input-item">
|
||||
<el-button :icon="isPlaying ? 'el-icon-video-pause' : 'el-icon-video-play'" @click="togglePlay">
|
||||
{{ isPlaying ? '暂停' : '播放' }}
|
||||
</el-button>
|
||||
|
||||
<el-button-group class="speed-control">
|
||||
<el-button icon="el-icon-minus" @click="changeSpeed('down')" :disabled="currentSpeedIndex === 0" />
|
||||
<el-button disabled>{{ playbackSpeed }}x</el-button>
|
||||
<el-button icon="el-icon-plus" @click="changeSpeed('up')"
|
||||
:disabled="currentSpeedIndex === speedOptions.length - 1" />
|
||||
</el-button-group>
|
||||
</div>
|
||||
|
||||
<!-- 进度条 -->
|
||||
<div class="input-item">
|
||||
<el-slider v-model="currentProgress" :max="trackPoints.length - 1" @change="onSliderChange"
|
||||
@input="onSliderChanging" :disabled="!trackPoints.length" />
|
||||
</div>
|
||||
|
||||
<!-- 轨迹点信息 -->
|
||||
<div class="track-info" v-if="currentPoint">
|
||||
<div class="info-item">
|
||||
<span class="label">时间:</span>
|
||||
<span class="value">{{ currentPointTime }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">速度:</span>
|
||||
<span class="value">{{ currentSpeed }} km/h</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">位置:</span>
|
||||
<span class="value">{{ location }}</span>
|
||||
</div>
|
||||
<!-- 新增状态显示 -->
|
||||
<div class="info-item">
|
||||
<span class="label">状态:</span>
|
||||
<span class="value">{{ currentStatus }}</span>
|
||||
</div>
|
||||
<!-- 新增电压显示 -->
|
||||
<div class="info-item">
|
||||
<span class="label">电压:</span>
|
||||
<span class="value">{{ currentBattery }}</span>
|
||||
</div>
|
||||
<!-- 新增锁状态显示 -->
|
||||
<div class="info-item">
|
||||
<span class="label">锁状态:</span>
|
||||
<span class="value">{{ currentLockStatus }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AMapLoader from "@amap/amap-jsapi-loader";
|
||||
import globalConfig from '@/utils/config/globalConfig'
|
||||
import { getArea } from '@/api/system/area'
|
||||
import { listParking } from '@/api/system/parking'
|
||||
import { getTrajectoryDetails, getDeviceBySn } from '@/api/system/device'
|
||||
|
||||
export default {
|
||||
name: "LocationMapss",
|
||||
props: {
|
||||
deviceSn: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
areaId: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: "100%"
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: "100%"
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
map: null,
|
||||
AMap: null,
|
||||
marker: null,
|
||||
markers: [],
|
||||
labels: [],
|
||||
polygon: [],
|
||||
|
||||
// 设备信息
|
||||
deviceInfo: null,
|
||||
currentLng: null,
|
||||
currentLat: null,
|
||||
currentStatus: '0',
|
||||
currentOnlineStatus: '0',
|
||||
|
||||
// 轨迹回放相关
|
||||
trackPoints: [],
|
||||
currentProgress: 0,
|
||||
isPlaying: false,
|
||||
playbackTimer: null,
|
||||
currentPointTime: '',
|
||||
|
||||
currentPoint: null,
|
||||
playbackSpeed: 1,
|
||||
speedOptions: [0.5, 1, 2, 4, 8],
|
||||
currentSpeedIndex: 1,
|
||||
currentSpeed: '0.00',
|
||||
playbackInterval: 1000,
|
||||
location: '',
|
||||
|
||||
// 时间选择相关
|
||||
startTime: '',
|
||||
endTime: '',
|
||||
activeTime: '3',
|
||||
timeOptions: [
|
||||
{ label: '1小时', value: '1' },
|
||||
{ label: '3小时', value: '3' },
|
||||
{ label: '6小时', value: '6' },
|
||||
{ label: '12小时', value: '12' }
|
||||
],
|
||||
|
||||
// 轨迹线相关
|
||||
polyline: null,
|
||||
passedPolyline: null,
|
||||
|
||||
// 区域相关
|
||||
area: null,
|
||||
parkingList: [],
|
||||
noParkingList: [],
|
||||
noridingList: [],
|
||||
parkingMarkers: [],
|
||||
parkingInfoWindows: [],
|
||||
parkingPolygons: [],
|
||||
currentStatus: '',
|
||||
currentBattery: '',
|
||||
currentLockStatus: ''
|
||||
}
|
||||
},
|
||||
|
||||
async mounted() {
|
||||
try {
|
||||
// 先获取设备信息
|
||||
await this.getDeviceInfo()
|
||||
// 设置默认时间范围
|
||||
this.initDefaultTime()
|
||||
// 获取区域信息
|
||||
await this.getAreas()
|
||||
// 获取轨迹数据(移到最后执行)
|
||||
|
||||
} catch (error) {
|
||||
console.error('初始化失败:', error)
|
||||
this.$message.error('初始化失败')
|
||||
}
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.stopPlayback()
|
||||
this.removeAllMarker()
|
||||
if (this.map) {
|
||||
this.map.destroy()
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async getAreas() {
|
||||
if (!this.areaId) return
|
||||
|
||||
getArea(this.areaId).then(response => {
|
||||
|
||||
console.log(response, 'responseresponse');
|
||||
this.area = response.data;
|
||||
|
||||
|
||||
listParking({ areaId: this.area.areaId }).then(response => {
|
||||
let list = response.rows;
|
||||
console.log(list, 'listlistlist');
|
||||
list.forEach(item => {
|
||||
if (item.type == '1') {
|
||||
this.parkingList.push(item);
|
||||
} else if (item.type == '2') {
|
||||
this.noParkingList.push(item);
|
||||
} else if (item.type == '3') {
|
||||
this.noridingList.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
this.initAMap();
|
||||
});
|
||||
});
|
||||
},
|
||||
formatStatus(status) {
|
||||
const statusMap = {
|
||||
'0': '仓库中',
|
||||
'1': '待租',
|
||||
'2': '预约中',
|
||||
'3': '骑行中',
|
||||
'4': '临时锁车中',
|
||||
'6': '调度中',
|
||||
'8': '下线'
|
||||
}
|
||||
return statusMap[status] || '未知状态'
|
||||
},
|
||||
|
||||
|
||||
// 修改 initAMap 方法
|
||||
async initAMap() {
|
||||
AMapLoader.load({
|
||||
key: globalConfig.aMap.key,
|
||||
version: "2.0",
|
||||
plugins: ["AMap.PlaceSearch", "AMap.MoveAnimation"],
|
||||
}).then((AMap) => {
|
||||
this.AMap = AMap;
|
||||
this.map = new AMap.Map("container", {
|
||||
viewMode: "3D",
|
||||
zoom: 16,
|
||||
center: [this.currentLng || 120.250452, this.currentLat || 27.101745],
|
||||
});
|
||||
|
||||
// 初始化区域和标记
|
||||
this.initMarker();
|
||||
}).catch((e) => {
|
||||
console.log("地图加载失败!!!");
|
||||
console.log(e);
|
||||
});
|
||||
},
|
||||
|
||||
initMarker() {
|
||||
this.removeAllMarker()
|
||||
if (this.currentLng != null && this.currentLat != null) {
|
||||
this.updateTrackData()
|
||||
this.addArea(JSON.parse(this.area.boundaryStr) || []);
|
||||
//停车区
|
||||
this.parkingList.forEach(parking => {
|
||||
this.addParking(JSON.parse(parking.boundaryStr) || [], parking.parkingName, parking.longitude, parking.latitude);
|
||||
this.addMarker2(parking, "https://lxnapi.ccttiot.com/FqcYf6ecsnbC0OT6YYAF5npgu-kh", parking.parkingName, "#1890ff");
|
||||
});
|
||||
//禁停区
|
||||
this.noParkingList.forEach(noparking => {
|
||||
this.addNoParking(JSON.parse(noparking.boundaryStr) || []);
|
||||
this.addMarker2(noparking, "https://lxnapi.ccttiot.com/FjKE5PWbnEnZUq3k-wVIvV4lv8Ab", noparking.parkingName, "#ff4444");
|
||||
});
|
||||
//禁行区
|
||||
this.noridingList.forEach(noriding => {
|
||||
this.addNoriding(JSON.parse(noriding.boundaryStr) || [], noriding.parkingName, noriding.longitude, noriding.latitude);
|
||||
this.addMarker2(noriding, "https://lxnapi.ccttiot.com/FmX1diEPPbFYe1vcUfKp6qbKzzh2", noriding.parkingName, "#ffcc00");
|
||||
});
|
||||
this.addMarker(this.currentLng, this.currentLat, this.deviceSn, this.deviceInfo.status, this.deviceInfo.onlineStatus)
|
||||
}
|
||||
},
|
||||
addMarker2(parking, icon, title, color) {
|
||||
let marker = new AMap.Marker({
|
||||
map: this.map,
|
||||
icon: new AMap.Icon({
|
||||
image: icon,
|
||||
size: new AMap.Size(25, 36), // 设置图标的宽高
|
||||
imageSize: new AMap.Size(25, 36) // 设置图标的实际显示尺寸
|
||||
}),
|
||||
position: [parking.longitude, parking.latitude],
|
||||
offset: new AMap.Pixel(-12.5, -36)
|
||||
});
|
||||
|
||||
this.markers.push(marker);
|
||||
|
||||
// console.log("title============="+title)
|
||||
// 创建一个 Text 实例来显示标题
|
||||
let text = new AMap.Text({
|
||||
text: title,
|
||||
anchor: 'center', // 设置文本的锚点
|
||||
position: [parking.longitude, parking.latitude],
|
||||
offset: new AMap.Pixel(0, -50),
|
||||
style: {
|
||||
'background-color': color, // 背景颜色为蓝色
|
||||
'border': 'none', // 边框颜色与背景一致
|
||||
'border-radius': '5px', // 圆角 5px
|
||||
'color': 'white', // 文字颜色为白色
|
||||
'font-size': '12px', // 字体大小
|
||||
}
|
||||
});
|
||||
|
||||
// 将文本标签添加到地图实例
|
||||
this.map.add(text);
|
||||
// console.log("text=============",text)
|
||||
this.labels.push(text)
|
||||
},
|
||||
formarStatus(status, onlineStatus) {
|
||||
if (onlineStatus == "0") {
|
||||
if (status == "3") {
|
||||
return globalConfig.icon.redyellow;
|
||||
} else if (status == "4") {
|
||||
return globalConfig.icon.orangered;
|
||||
}
|
||||
return globalConfig.icon.red;
|
||||
} else {
|
||||
if (status == "0") {
|
||||
return globalConfig.icon.gray;
|
||||
} else if (status == "1") {
|
||||
return globalConfig.icon.blue;
|
||||
} else if (status == "2") {
|
||||
return globalConfig.icon.green;
|
||||
} else if (status == "3") {
|
||||
return globalConfig.icon.yellow;
|
||||
} else if (status == "4") {
|
||||
return globalConfig.icon.orange;
|
||||
} else if (status == "8") {
|
||||
return globalConfig.icon.gray;
|
||||
} else if (status == "9") {
|
||||
return globalConfig.icon.gray;
|
||||
}
|
||||
}
|
||||
},
|
||||
addMarker: function (lng, lat, title, status, onlineStatus) {
|
||||
//创建一个 Marker 实例:
|
||||
console.log(status, onlineStatus, 'status,onlineStatus');
|
||||
|
||||
let icon = this.formarStatus(status, onlineStatus)
|
||||
console.log('icon===========' + icon)
|
||||
let marker = new AMap.Marker({
|
||||
position: new AMap.LngLat(lng, lat), //经纬度对象
|
||||
icon: new AMap.Icon({
|
||||
image: icon,
|
||||
size: new AMap.Size(25, 30), // 设置图标的宽高
|
||||
imageSize: new AMap.Size(25, 30) // 设置图标的实际显示尺寸
|
||||
}),
|
||||
title: title,
|
||||
offset: new AMap.Pixel(-12.5, -35)
|
||||
})
|
||||
|
||||
//将创建的点标记添加到已有的地图实例:
|
||||
this.map.add(marker)
|
||||
this.markers.push(marker)
|
||||
console.log();
|
||||
|
||||
// 创建一个 Text 实例来显示标题
|
||||
let text = new AMap.Text({
|
||||
text: title,
|
||||
anchor: 'center', // 设置文本的锚点
|
||||
position: new AMap.LngLat(lng, lat), // 经纬度对象
|
||||
offset: new AMap.Pixel(0, -50),
|
||||
style: {
|
||||
'background-color': '#1890ff', // 背景颜色为蓝色
|
||||
'border': 'none', // 边框颜色与背景一致
|
||||
'border-radius': '5px', // 圆角 5px
|
||||
'color': 'white', // 文字颜色为白色
|
||||
'font-size': '14px', // 字体大小
|
||||
'padding': '5px 10px' // 内边距,调整文本框的大小
|
||||
}
|
||||
})
|
||||
|
||||
// 将文本标签添加到地图实例
|
||||
this.map.add(text)
|
||||
},
|
||||
addParking(data, title, lon, lat) {
|
||||
let polygon = new this.AMap.Polygon({
|
||||
path: data,
|
||||
fillColor: '#ccebc5',
|
||||
strokeOpacity: 1,
|
||||
fillOpacity: 0.5,
|
||||
strokeColor: '#3b7ed9',
|
||||
strokeWeight: 2,
|
||||
strokeStyle: 'solid',
|
||||
strokeDasharray: [5, 5],
|
||||
})
|
||||
|
||||
polygon.on('mouseover', () => {
|
||||
polygon.setOptions({
|
||||
fillOpacity: 0.7,
|
||||
fillColor: '#71b7cc'
|
||||
})
|
||||
})
|
||||
|
||||
polygon.on('mouseout', () => {
|
||||
polygon.setOptions({
|
||||
fillOpacity: 0.5,
|
||||
fillColor: '#a7c1d0'
|
||||
})
|
||||
})
|
||||
|
||||
this.map.add(polygon)
|
||||
},
|
||||
|
||||
addNoParking(data) {
|
||||
let polygon = new this.AMap.Polygon({
|
||||
path: data,
|
||||
fillColor: '#ccebc5',
|
||||
strokeOpacity: 1,
|
||||
fillOpacity: 0.5,
|
||||
strokeColor: '#ff0000',
|
||||
strokeWeight: 2,
|
||||
strokeStyle: 'solid',
|
||||
strokeDasharray: [5, 5],
|
||||
})
|
||||
|
||||
polygon.on('mouseover', () => {
|
||||
polygon.setOptions({
|
||||
fillOpacity: 0.7,
|
||||
fillColor: '#ff0000'
|
||||
})
|
||||
})
|
||||
|
||||
polygon.on('mouseout', () => {
|
||||
polygon.setOptions({
|
||||
fillOpacity: 0.5,
|
||||
fillColor: '#cc7b7b'
|
||||
})
|
||||
})
|
||||
|
||||
this.map.add(polygon)
|
||||
},
|
||||
|
||||
addNoriding(data) {
|
||||
let polygon = new this.AMap.Polygon({
|
||||
path: data,
|
||||
fillColor: '#ccebc5',
|
||||
strokeOpacity: 1,
|
||||
fillOpacity: 0.5,
|
||||
strokeColor: '#ffcc00',
|
||||
strokeWeight: 2,
|
||||
strokeStyle: 'solid',
|
||||
strokeDasharray: [5, 5],
|
||||
})
|
||||
|
||||
polygon.on('mouseover', () => {
|
||||
polygon.setOptions({
|
||||
fillOpacity: 0.7,
|
||||
fillColor: '#FFEBA4FF'
|
||||
})
|
||||
})
|
||||
|
||||
polygon.on('mouseout', () => {
|
||||
polygon.setOptions({
|
||||
fillOpacity: 0.5,
|
||||
fillColor: '#ffeba4'
|
||||
})
|
||||
})
|
||||
|
||||
this.map.add(polygon)
|
||||
},
|
||||
|
||||
addArea(data) {
|
||||
let polygon = new this.AMap.Polygon({
|
||||
path: data,
|
||||
fillColor: '#ccebc5',
|
||||
strokeOpacity: 1,
|
||||
fillOpacity: 0.5,
|
||||
strokeColor: '#2b8cbe',
|
||||
strokeWeight: 1,
|
||||
strokeStyle: 'dashed',
|
||||
strokeDasharray: [5, 5],
|
||||
})
|
||||
|
||||
this.map.add(polygon)
|
||||
},
|
||||
|
||||
removeAllMarker() {
|
||||
if (this.map) {
|
||||
this.map.remove(this.markers)
|
||||
this.map.remove(this.labels)
|
||||
this.map.remove(this.polygon)
|
||||
}
|
||||
this.markers = []
|
||||
this.labels = []
|
||||
this.polygon = []
|
||||
},
|
||||
|
||||
getMarkerIcon(status, onlineStatus) {
|
||||
let iconUrl = ''
|
||||
if (onlineStatus === "0") {
|
||||
if (status === "3") {
|
||||
iconUrl = globalConfig.icon.redyellow
|
||||
} else if (status === "4") {
|
||||
iconUrl = globalConfig.icon.orangered
|
||||
} else {
|
||||
iconUrl = globalConfig.icon.red
|
||||
}
|
||||
} else {
|
||||
switch (status) {
|
||||
case "0": iconUrl = globalConfig.icon.gray; break
|
||||
case "1": iconUrl = globalConfig.icon.blue; break
|
||||
case "2": iconUrl = globalConfig.icon.green; break
|
||||
case "3": iconUrl = globalConfig.icon.yellow; break
|
||||
case "4": iconUrl = globalConfig.icon.orange; break
|
||||
case "8":
|
||||
case "9": iconUrl = globalConfig.icon.gray; break
|
||||
default: iconUrl = globalConfig.icon.gray
|
||||
}
|
||||
}
|
||||
|
||||
return new this.AMap.Icon({
|
||||
image: iconUrl,
|
||||
size: new this.AMap.Size(25, 38),
|
||||
imageSize: new this.AMap.Size(25, 38)
|
||||
})
|
||||
},
|
||||
|
||||
async getDeviceInfo() {
|
||||
try {
|
||||
const response = await getDeviceBySn(this.deviceSn)
|
||||
// 检查数据是否存在
|
||||
if (response?.data) {
|
||||
this.deviceInfo = response.data
|
||||
const { longitude, latitude, status, onlineStatus } = response.data
|
||||
|
||||
this.currentLng = longitude
|
||||
this.currentLat = latitude
|
||||
this.currentStatus = status
|
||||
this.currentOnlineStatus = onlineStatus
|
||||
|
||||
if (longitude && latitude) {
|
||||
// this.showCurrentDevicePosition()
|
||||
}
|
||||
} else {
|
||||
console.warn('设备信息为空')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取设备信息失败1111:', error)
|
||||
// 只在真正的错误时显示错误提示
|
||||
if (error?.response?.status !== 200) {
|
||||
this.$message.error('获取设备信息失败')
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// showCurrentDevicePosition() {
|
||||
// if (this.currentLng && this.currentLat) {
|
||||
// if (this.marker) {
|
||||
// this.map.remove(this.marker)
|
||||
// }
|
||||
|
||||
// this.marker = new this.AMap.Marker({
|
||||
// position: new this.AMap.LngLat(this.currentLng, this.currentLat),
|
||||
// icon: this.getMarkerIcon(this.currentStatus, this.currentOnlineStatus),
|
||||
// offset: new this.AMap.Pixel(-12.5, -19.5)
|
||||
// })
|
||||
|
||||
// this.map.add(this.marker)
|
||||
// this.map.setCenter(new this.AMap.LngLat(this.currentLng, this.currentLat))
|
||||
// }
|
||||
// },
|
||||
|
||||
// 轨迹回放相关方法
|
||||
initDefaultTime() {
|
||||
const now = new Date()
|
||||
const threeHoursAgo = new Date(now - 3 * 60 * 60 * 1000)
|
||||
this.startTime = this.formatDateTime(threeHoursAgo)
|
||||
this.endTime = this.formatDateTime(now)
|
||||
},
|
||||
|
||||
async updateTrackData() {
|
||||
try {
|
||||
const params = {
|
||||
sn: this.deviceSn,
|
||||
startTime: this.startTime,
|
||||
endTime: this.endTime
|
||||
}
|
||||
|
||||
const res = await getTrajectoryDetails(params)
|
||||
if (res.code === 200) {
|
||||
if (!res.data?.length) {
|
||||
this.$message.warning('该时间段内无轨迹数据')
|
||||
this.clearTrackData()
|
||||
return
|
||||
}
|
||||
|
||||
// 确保数据处理和绘制都执行
|
||||
this.trackPoints = res.data.map(point => ({
|
||||
latitude: parseFloat(point.latitude),
|
||||
longitude: parseFloat(point.longitude),
|
||||
time: point.at,
|
||||
status: point.status,
|
||||
onlineStatus: point.onlineStatus
|
||||
}))
|
||||
|
||||
// 立即绘制轨迹线和起终点标记
|
||||
this.drawTrackLine()
|
||||
// 重置播放位置
|
||||
this.resetPlayback()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取轨迹数据失败:", error)
|
||||
this.$message.error('获取轨迹数据失败')
|
||||
}
|
||||
},
|
||||
|
||||
processTrackData(data) {
|
||||
this.trackPoints = data.map(point => ({
|
||||
latitude: parseFloat(point.latitude),
|
||||
longitude: parseFloat(point.longitude),
|
||||
time: point.at,
|
||||
status: point.status,
|
||||
onlineStatus: point.onlineStatus
|
||||
}))
|
||||
|
||||
this.drawTrackLine()
|
||||
this.resetPlayback()
|
||||
},
|
||||
drawTrackLine() {
|
||||
if (!this.trackPoints.length) return
|
||||
|
||||
const path = this.trackPoints.map(point => [point.longitude, point.latitude])
|
||||
|
||||
// 清除现有的线和标记
|
||||
if (this.polyline) {
|
||||
this.map.remove(this.polyline)
|
||||
}
|
||||
|
||||
// 添加轨迹线
|
||||
this.polyline = new this.AMap.Polyline({
|
||||
path: path,
|
||||
strokeColor: "#28F",
|
||||
showDir: true,
|
||||
strokeWeight: 6
|
||||
})
|
||||
|
||||
// 创建起点标记
|
||||
const startMarker = new this.AMap.Marker({
|
||||
position: path[0],
|
||||
icon: new this.AMap.Icon({
|
||||
image: 'https://lxnapi.ccttiot.com/bike/img/static/u06paUGiHLvL08Pw7BGr',
|
||||
size: new this.AMap.Size(25, 36),
|
||||
imageSize: new this.AMap.Size(25, 36)
|
||||
}),
|
||||
offset: new this.AMap.Pixel(-12.5, -36)
|
||||
})
|
||||
|
||||
// 创建终点标记
|
||||
const endMarker = new this.AMap.Marker({
|
||||
position: path[path.length - 1],
|
||||
icon: new this.AMap.Icon({
|
||||
image: 'https://lxnapi.ccttiot.com/bike/img/static/uwpAj9vYtPRmhtTOtflx',
|
||||
size: new this.AMap.Size(25, 36),
|
||||
imageSize: new this.AMap.Size(25, 36)
|
||||
}),
|
||||
offset: new this.AMap.Pixel(-12.5, -36)
|
||||
})
|
||||
|
||||
// 保存标记引用以便后续清除
|
||||
this.startMarker = startMarker
|
||||
this.endMarker = endMarker
|
||||
|
||||
// 一次性添加所有元素
|
||||
this.map.add([this.polyline, startMarker, endMarker])
|
||||
this.map.setFitView([this.polyline, startMarker, endMarker])
|
||||
},
|
||||
resetPlayback() {
|
||||
this.currentProgress = 0
|
||||
this.updateMapPoint(0)
|
||||
},
|
||||
clearTrackData() {
|
||||
if (this.polyline) {
|
||||
this.map.remove(this.polyline)
|
||||
}
|
||||
if (this.passedPolyline) {
|
||||
this.map.remove(this.passedPolyline)
|
||||
}
|
||||
if (this.marker) {
|
||||
this.map.remove(this.marker)
|
||||
}
|
||||
// 清除起终点标记
|
||||
if (this.startMarker) {
|
||||
this.map.remove(this.startMarker)
|
||||
}
|
||||
if (this.endMarker) {
|
||||
this.map.remove(this.endMarker)
|
||||
}
|
||||
|
||||
this.trackPoints = []
|
||||
this.currentProgress = 0
|
||||
this.currentPoint = null
|
||||
this.currentPointTime = ''
|
||||
this.currentSpeed = '0.00'
|
||||
this.location = ''
|
||||
this.currentStatus = ''
|
||||
this.currentBattery = ''
|
||||
this.currentLockStatus = ''
|
||||
},
|
||||
|
||||
updateMapPoint(index) {
|
||||
if (!this.trackPoints.length || index >= this.trackPoints.length) return
|
||||
|
||||
const point = this.trackPoints[index]
|
||||
this.currentPoint = point
|
||||
this.currentPointTime = point.time
|
||||
this.location = `${point.longitude}, ${point.latitude}`
|
||||
|
||||
// 添加新字段的更新
|
||||
this.currentStatus = this.formatStatus(point.status)
|
||||
this.currentBattery = point.bta == null ? '未知' : (point.bta / 10).toFixed(1) + 'V'
|
||||
this.currentLockStatus = point.lockStatus == '0' ? '关锁' : '开锁'
|
||||
|
||||
if (index > 0) {
|
||||
const prevPoint = this.trackPoints[index - 1]
|
||||
const distance = this.calculateDistance(
|
||||
prevPoint.latitude,
|
||||
prevPoint.longitude,
|
||||
point.latitude,
|
||||
point.longitude
|
||||
)
|
||||
const timeGap = (new Date(point.time) - new Date(prevPoint.time)) / 1000
|
||||
this.currentSpeed = timeGap > 0 ? ((distance / timeGap) * 3.6).toFixed(1) : '0.00'
|
||||
}
|
||||
|
||||
this.updatePassedPolyline(index)
|
||||
this.updateMarkerPosition(point)
|
||||
},
|
||||
|
||||
updatePassedPolyline(index) {
|
||||
const passedPath = this.trackPoints
|
||||
.slice(0, index + 1)
|
||||
.map(p => [p.longitude, p.latitude])
|
||||
|
||||
if (!this.passedPolyline) {
|
||||
this.passedPolyline = new this.AMap.Polyline({
|
||||
path: passedPath,
|
||||
strokeColor: "#AF5",
|
||||
strokeWeight: 6
|
||||
})
|
||||
this.map.add(this.passedPolyline)
|
||||
} else {
|
||||
this.passedPolyline.setPath(passedPath)
|
||||
}
|
||||
},
|
||||
|
||||
updateMarkerPosition(point) {
|
||||
if (!this.marker) {
|
||||
this.marker = new this.AMap.Marker({
|
||||
position: [point.longitude, point.latitude],
|
||||
icon: this.getMarkerIcon(point.status, point.onlineStatus),
|
||||
offset: new this.AMap.Pixel(-12.5, -19.5)
|
||||
})
|
||||
this.map.add(this.marker)
|
||||
} else {
|
||||
this.marker.setPosition([point.longitude, point.latitude])
|
||||
this.marker.setIcon(this.getMarkerIcon(point.status, point.onlineStatus))
|
||||
}
|
||||
},
|
||||
|
||||
// 播放控制
|
||||
togglePlay() {
|
||||
this.isPlaying = !this.isPlaying
|
||||
this.isPlaying ? this.startPlayback() : this.pausePlayback()
|
||||
},
|
||||
|
||||
startPlayback() {
|
||||
if (this.playbackTimer) return
|
||||
|
||||
this.playbackTimer = setInterval(() => {
|
||||
if (this.currentProgress >= this.trackPoints.length - 1) {
|
||||
this.pausePlayback()
|
||||
this.isPlaying = false
|
||||
// 添加延时,让最后一个点的位置显示片刻后再回到起点
|
||||
setTimeout(() => {
|
||||
this.currentProgress = 0
|
||||
this.updateMapPoint(0)
|
||||
}, 1000)
|
||||
return
|
||||
}
|
||||
this.currentProgress++
|
||||
this.updateMapPoint(this.currentProgress)
|
||||
}, this.playbackInterval / this.playbackSpeed)
|
||||
},
|
||||
|
||||
pausePlayback() {
|
||||
if (this.playbackTimer) {
|
||||
clearInterval(this.playbackTimer)
|
||||
this.playbackTimer = null
|
||||
}
|
||||
},
|
||||
|
||||
stopPlayback() {
|
||||
this.pausePlayback()
|
||||
this.isPlaying = false
|
||||
this.currentProgress = 0
|
||||
this.updateMapPoint(0) // 更新到起始点,而不是清除轨迹
|
||||
},
|
||||
|
||||
changeSpeed(direction) {
|
||||
if (direction === 'up' && this.currentSpeedIndex < this.speedOptions.length - 1) {
|
||||
this.currentSpeedIndex++
|
||||
} else if (direction === 'down' && this.currentSpeedIndex > 0) {
|
||||
this.currentSpeedIndex--
|
||||
}
|
||||
|
||||
this.playbackSpeed = this.speedOptions[this.currentSpeedIndex]
|
||||
|
||||
if (this.isPlaying) {
|
||||
this.pausePlayback()
|
||||
this.startPlayback()
|
||||
}
|
||||
},
|
||||
|
||||
// 时间选择
|
||||
handleTimeChange() {
|
||||
this.activeTime = ''
|
||||
this.updateTrackData()
|
||||
},
|
||||
|
||||
selectTime(time) {
|
||||
this.activeTime = time
|
||||
const hours = parseInt(time)
|
||||
const now = new Date()
|
||||
const startTime = new Date(now - hours * 60 * 60 * 1000)
|
||||
|
||||
this.startTime = this.formatDateTime(startTime)
|
||||
this.endTime = this.formatDateTime(now)
|
||||
this.updateTrackData()
|
||||
},
|
||||
|
||||
// 工具方法
|
||||
formatDateTime(date) {
|
||||
const pad = num => String(num).padStart(2, '0')
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:00`
|
||||
},
|
||||
|
||||
calculateDistance(lat1, lng1, lat2, lng2) {
|
||||
const R = 6371
|
||||
const toRad = deg => deg * Math.PI / 180
|
||||
const dLat = toRad(lat2 - lat1)
|
||||
const dLng = toRad(lng2 - lng1)
|
||||
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) *
|
||||
Math.sin(dLng / 2) * Math.sin(dLng / 2)
|
||||
return 2 * R * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
|
||||
},
|
||||
|
||||
onSliderChange(value) {
|
||||
this.currentProgress = value
|
||||
this.updateMapPoint(value)
|
||||
},
|
||||
|
||||
onSliderChanging(value) {
|
||||
this.updateMapPoint(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.place-search-map {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
#container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.input-card {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
bottom: 20px;
|
||||
padding: 20px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
width: 430px;
|
||||
z-index: 100;
|
||||
|
||||
h4 {
|
||||
margin: 0 0 16px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.input-item {
|
||||
margin-bottom: 16px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.time-range {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
gap: 8px;
|
||||
|
||||
.separator {
|
||||
color: #606266;
|
||||
}
|
||||
}
|
||||
|
||||
.quick-select {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.speed-control {
|
||||
margin-left: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.track-info {
|
||||
margin-top: 16px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #EBEEF5;
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.label {
|
||||
width: 60px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.value {
|
||||
flex: 1;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.info-window {
|
||||
padding: 8px;
|
||||
|
||||
h4 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -232,6 +232,20 @@ export const dynamicRoutes = [
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/system/deviceDetail',
|
||||
component: Layout,
|
||||
hidden: true,
|
||||
permissions: ['system:dept:list'],
|
||||
children: [
|
||||
{
|
||||
path: 'index/:sn(\\d+)?', // 将参数设置为可选
|
||||
component: () => import('@/views/system/device/device_detail'),
|
||||
name: 'Data',
|
||||
meta: { title: '设备详情', activeMenu: '/system/device' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/system/area-parking',
|
||||
component: Layout,
|
||||
|
|
|
@ -530,6 +530,11 @@ export default {
|
|||
created() {
|
||||
console.log("当前用户信息:", this.$store.state.user.name)
|
||||
this.userName = this.$store.state.user.name;
|
||||
const areaName = this.$route.query.areaName
|
||||
console.log(areaName,'areaName');
|
||||
if(areaName){
|
||||
this.queryParams.areaName = areaName
|
||||
}
|
||||
this.getList();
|
||||
this.getDeptTree();
|
||||
if(this.userName === 'admin'){
|
||||
|
|
|
@ -2,54 +2,25 @@
|
|||
<div class="app-container">
|
||||
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
|
||||
<el-form-item label="命令" prop="command">
|
||||
<el-input
|
||||
v-model="queryParams.command"
|
||||
placeholder="请输入命令"
|
||||
clearable
|
||||
@keyup.enter.native="handleQuery"
|
||||
/>
|
||||
<el-input v-model="queryParams.command" placeholder="请输入命令" clearable @keyup.enter.native="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="MAC" prop="mac">
|
||||
<el-input
|
||||
v-model="queryParams.mac"
|
||||
placeholder="请输入mac"
|
||||
clearable
|
||||
@keyup.enter.native="handleQuery"
|
||||
/>
|
||||
<el-form-item label="MAC" prop="mac" v-if="type != 'device'">
|
||||
<el-input v-model="queryParams.mac" placeholder="请输入mac" clearable @keyup.enter.native="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="SN" prop="sn">
|
||||
<el-input
|
||||
v-model="queryParams.sn"
|
||||
placeholder="请输入sn"
|
||||
clearable
|
||||
@keyup.enter.native="handleQuery"
|
||||
/>
|
||||
<el-form-item label="SN" prop="sn" v-if="type != 'device'">
|
||||
<el-input v-model="queryParams.sn" placeholder="请输入sn" clearable @keyup.enter.native="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="类型" prop="type">
|
||||
<el-input
|
||||
v-model="queryParams.type"
|
||||
placeholder="请输入类型"
|
||||
clearable
|
||||
@keyup.enter.native="handleQuery"
|
||||
/>
|
||||
<el-input v-model="queryParams.type" placeholder="请输入类型" clearable @keyup.enter.native="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="回调状态" prop="callStatus">
|
||||
<el-select v-model="queryParams.callStatus" placeholder="请选择回调状态" clearable>
|
||||
<el-option
|
||||
v-for="dict in dict.type.onenet_call_status"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
<el-option v-for="dict in dict.type.onenet_call_status" :key="dict.value" :label="dict.label"
|
||||
:value="dict.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="订单号" prop="orderNo">
|
||||
<el-input
|
||||
v-model="queryParams.orderNo"
|
||||
placeholder="请输入订单号"
|
||||
clearable
|
||||
@keyup.enter.native="handleQuery"
|
||||
/>
|
||||
<el-input v-model="queryParams.orderNo" placeholder="请输入订单号" clearable @keyup.enter.native="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="操作人" prop="createBy">
|
||||
<el-input
|
||||
|
@ -74,40 +45,30 @@
|
|||
<span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- <el-table-column label="请求url" align="center" prop="url" :show-overflow-tooltip="true"/>-->
|
||||
<!-- <el-table-column label="命令" align="center" prop="command" />-->
|
||||
<!-- <el-table-column label="请求url" align="center" prop="url" :show-overflow-tooltip="true"/>-->
|
||||
<!-- <el-table-column label="命令" align="center" prop="command" />-->
|
||||
<el-table-column label="类型" align="center" prop="type" />
|
||||
<el-table-column label="mac" align="center" prop="mac" />
|
||||
<el-table-column label="sn" align="center" prop="sn" />
|
||||
<el-table-column label="订单号" align="center" prop="orderNo" :show-overflow-tooltip="true"/>
|
||||
<el-table-column label="响应" align="center" prop="result" :show-overflow-tooltip="true"/>
|
||||
<el-table-column label="回调状态" align="center" prop="callStatus" >
|
||||
<el-table-column label="订单号" align="center" prop="orderNo" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="响应" align="center" prop="result" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="回调状态" align="center" prop="callStatus">
|
||||
<template slot-scope="scope">
|
||||
<dict-tag :options="dict.type.onenet_call_status" :value="scope.row.callStatus"/>
|
||||
<dict-tag :options="dict.type.onenet_call_status" :value="scope.row.callStatus" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作人" align="center" prop="createBy" />
|
||||
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-view"
|
||||
@click="handleView(scope.row,scope.index)"
|
||||
v-hasPermi="['system:commandLog:query']"
|
||||
>详细</el-button>
|
||||
<el-button size="mini" type="text" icon="el-icon-view" @click="handleView(scope.row, scope.index)"
|
||||
v-hasPermi="['system:commandLog:query']">详细</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<pagination
|
||||
v-show="total>0"
|
||||
:total="total"
|
||||
:page.sync="queryParams.pageNum"
|
||||
:limit.sync="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize"
|
||||
@pagination="getList" />
|
||||
|
||||
<!-- 添加或修改命令日志对话框 -->
|
||||
<el-dialog title="命令日志详细" :visible.sync="open" width="800px" append-to-body>
|
||||
|
@ -138,7 +99,7 @@
|
|||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="回调状态">
|
||||
<dict-tag :options="dict.type.onenet_call_status" :value="form.callStatus"/>
|
||||
<dict-tag :options="dict.type.onenet_call_status" :value="form.callStatus" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
|
@ -157,7 +118,8 @@ import { listCommandLog, getCommandLog, delCommandLog, addCommandLog, updateComm
|
|||
|
||||
export default {
|
||||
name: "CommandLog",
|
||||
dicts: ['onenet_call_status'],
|
||||
dicts: ['onenet_call_status'],
|
||||
props: ['sn', 'type'],
|
||||
data() {
|
||||
return {
|
||||
// 遮罩层
|
||||
|
@ -197,15 +159,23 @@ export default {
|
|||
};
|
||||
},
|
||||
created() {
|
||||
const sn = this.$route.params && this.$route.params.sn;
|
||||
if (sn != null) {
|
||||
this.queryParams.sn = sn;
|
||||
if (this.type == 'device') {
|
||||
this.queryParams.sn = this.sn;
|
||||
this.getList();
|
||||
} else {
|
||||
const sn = this.$route.params && this.$route.params.sn;
|
||||
if (sn != null) {
|
||||
this.queryParams.sn = sn;
|
||||
} else {
|
||||
|
||||
}
|
||||
const orderNo = this.$route.params && this.$route.params.orderNo;
|
||||
if (orderNo != null) {
|
||||
this.queryParams.orderNo = orderNo;
|
||||
}
|
||||
this.getList();
|
||||
}
|
||||
const orderNo = this.$route.params && this.$route.params.orderNo;
|
||||
if (orderNo != null) {
|
||||
this.queryParams.orderNo = orderNo;
|
||||
}
|
||||
this.getList();
|
||||
|
||||
},
|
||||
methods: {
|
||||
/** 查询命令日志列表 */
|
||||
|
@ -251,7 +221,7 @@ export default {
|
|||
// 多选框选中数据
|
||||
handleSelectionChange(selection) {
|
||||
this.ids = selection.map(item => item.id)
|
||||
this.single = selection.length!==1
|
||||
this.single = selection.length !== 1
|
||||
this.multiple = !selection.length
|
||||
},
|
||||
/** 新增按钮操作 */
|
||||
|
@ -293,12 +263,12 @@ export default {
|
|||
/** 删除按钮操作 */
|
||||
handleDelete(row) {
|
||||
const ids = row.id || this.ids;
|
||||
this.$modal.confirm('是否确认删除命令日志编号为"' + ids + '"的数据项?').then(function() {
|
||||
this.$modal.confirm('是否确认删除命令日志编号为"' + ids + '"的数据项?').then(function () {
|
||||
return delCommandLog(ids);
|
||||
}).then(() => {
|
||||
this.getList();
|
||||
this.$modal.msgSuccess("删除成功");
|
||||
}).catch(() => {});
|
||||
}).catch(() => { });
|
||||
},
|
||||
/** 导出按钮操作 */
|
||||
handleExport() {
|
||||
|
|
|
@ -3,28 +3,27 @@
|
|||
<el-row type="flex">
|
||||
<el-tabs style="width: 100%">
|
||||
<el-tab-pane label="月报表">
|
||||
<!-- 使用 el-date-picker 组件 -->
|
||||
<el-date-picker v-model="queryParams.date" type="month" format="yyyy年MM月" value-format="yyyy-MM"
|
||||
placeholder="选择年月" @change="onChangeYear" style="width: 150px" />
|
||||
<el-date-picker
|
||||
v-model="queryParams.date"
|
||||
type="month"
|
||||
format="yyyy年MM月"
|
||||
value-format="yyyy-MM"
|
||||
placeholder="选择年月"
|
||||
@change="onChangeYear"
|
||||
style="width: 150px"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-row>
|
||||
<!-- <single-line-chart
|
||||
v-loading="loading"
|
||||
:labels="labels"
|
||||
:chart-data="chartData"
|
||||
name="收入(元)"
|
||||
height="268px"
|
||||
/> -->
|
||||
<div id="myChart" style="width: 100%;height: 19.17rem;"></div>
|
||||
<div id="myChart" style="width: 100%;height: 19.17rem;" v-loading="loading"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getIncomeList } from '@/api/system/dept'
|
||||
import SingleLineChart from "@/components/SingleLineChart/index.vue";
|
||||
import { BonusArrivalType } from "@/utils/constants";
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
export default {
|
||||
name: 'userRechargeReport',
|
||||
components: { SingleLineChart },
|
||||
|
@ -43,25 +42,65 @@ export default {
|
|||
labels: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
|
||||
chartData: [],
|
||||
StatisticsInfo: null,
|
||||
myChart: null,
|
||||
queryParams: {
|
||||
// 设置默认日期格式为 "YYYY-MM"
|
||||
date: `${currentYear}-${currentMonth.toString().padStart(2, '0')}`,
|
||||
deptId: this.deptId,
|
||||
// arrivalTypes: BonusArrivalType.userList()
|
||||
},
|
||||
loading: false,
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
deptId(newVal) {
|
||||
this.getReportData(newVal);
|
||||
deptId: {
|
||||
handler(newVal) {
|
||||
this.initData(newVal);
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.myChart = echarts.init(document.getElementById("myChart"));
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
if (this.myChart) {
|
||||
this.myChart.dispose();
|
||||
}
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
},
|
||||
|
||||
methods: {
|
||||
handleResize() {
|
||||
if (this.myChart) {
|
||||
this.myChart.resize();
|
||||
}
|
||||
},
|
||||
|
||||
async initData(deptId) {
|
||||
if (!deptId) return;
|
||||
|
||||
this.loading = true;
|
||||
try {
|
||||
// 第一次调用
|
||||
await this.getReportData(deptId);
|
||||
// 延迟 500ms 后进行第二次调用
|
||||
setTimeout(async () => {
|
||||
await this.getReportData(deptId);
|
||||
}, 500);
|
||||
} catch (error) {
|
||||
console.error('初始化数据失败:', error);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
drawLine() {
|
||||
let myChart = echarts.init(document.getElementById("myChart"));
|
||||
// 设置图表的配置项和数据
|
||||
myChart.setOption({
|
||||
if (!this.myChart || !this.StatisticsInfo) return;
|
||||
|
||||
const option = {
|
||||
grid: {
|
||||
left: 60,
|
||||
right: 50,
|
||||
|
@ -84,7 +123,7 @@ export default {
|
|||
type: 'value',
|
||||
name: '营收',
|
||||
position: 'left',
|
||||
min: 0, // 确保“营收”y轴从0开始
|
||||
min: 0,
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#5470C6'
|
||||
|
@ -98,7 +137,7 @@ export default {
|
|||
type: 'value',
|
||||
name: '订单',
|
||||
position: 'right',
|
||||
min: 0, // 确保“订单”y轴从0开始
|
||||
min: 0,
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#91CC75'
|
||||
|
@ -131,41 +170,34 @@ export default {
|
|||
smooth: true
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
,
|
||||
};
|
||||
|
||||
this.myChart.setOption(option);
|
||||
},
|
||||
|
||||
onChangeYear(value) {
|
||||
if (value) {
|
||||
this.queryParams.payTimeYear = value; // 更新为选择的日期 (格式化为 YYYY-MM)
|
||||
this.getReportData(this.deptId);
|
||||
this.queryParams.payTimeYear = value;
|
||||
this.initData(this.deptId);
|
||||
}
|
||||
},
|
||||
getReportData(deptId) {
|
||||
this.loading = true;
|
||||
this.queryParams.deptId = deptId || this.deptId;
|
||||
|
||||
getIncomeList(this.queryParams).then(response => {
|
||||
this.StatisticsInfo = response.data
|
||||
setTimeout(() => {
|
||||
this.drawLine()
|
||||
}, 200);
|
||||
|
||||
const data = response.data;
|
||||
const list = [];
|
||||
async getReportData(deptId) {
|
||||
this.queryParams.deptId = deptId;
|
||||
|
||||
if (data && data.length > 0) {
|
||||
// 遍历接口返回的数据,每天的收入数据按顺序加入列表
|
||||
data.forEach(item => {
|
||||
list.push(item.income); // 将每天的收入数据放入 list
|
||||
});
|
||||
try {
|
||||
const response = await getIncomeList(this.queryParams);
|
||||
if (response.data) {
|
||||
this.StatisticsInfo = response.data;
|
||||
this.drawLine();
|
||||
}
|
||||
|
||||
// 更新 chartData,用于 single-line-chart 显示
|
||||
this.chartData = list;
|
||||
}).finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error("获取报表数据失败:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
getReportMonth() {
|
||||
const now = new Date();
|
||||
const nowYear = now.getFullYear();
|
||||
|
@ -186,4 +218,4 @@ export default {
|
|||
.title {
|
||||
line-height: 36px;
|
||||
}
|
||||
</style>
|
||||
</style>
|
|
@ -232,7 +232,7 @@
|
|||
},
|
||||
{
|
||||
label: '仓库',
|
||||
offlineCount: (vo.allOfflineNum || 0) - (offline.offlineNum || 0),
|
||||
offlineCount: (vo.allOfflineNum || 0) - (vo.offlineNum || 0),
|
||||
totalCount: vo.inStashNum || 0
|
||||
},
|
||||
{
|
||||
|
|
|
@ -1,93 +1,286 @@
|
|||
<template>
|
||||
<div>
|
||||
<el-table v-loading="loading" :data="orderList">
|
||||
<el-table-column align="center" type="index" label="#"></el-table-column>
|
||||
<el-table-column align="center" label="订单编号" width="200" prop="orderNo">
|
||||
<template slot-scope="scope">
|
||||
<router-link :to="'/system/order/index/' + scope.row.orderNo" class="link-type"
|
||||
@click.native="closeAllDialogs">
|
||||
<span>{{ scope.row.orderNo }}</span>
|
||||
</router-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="使用用户" prop="userName"></el-table-column>
|
||||
<el-table-column align="center" label="手机号" prop="phonenumber"></el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template slot-scope="scope">
|
||||
<dict-tag :options="dict.type.et_order_status" :value="scope.row.status"/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="支付费用(元)" prop="totalFee">
|
||||
<template slot-scope="d">{{d.row.totalFee}} 元</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="下单时间" prop="createTime"></el-table-column>
|
||||
</el-table>
|
||||
<div class="order-record-container">
|
||||
<div v-loading="loading" class="record-list">
|
||||
<!-- 列表为空时显示 -->
|
||||
<el-empty v-if="!orderList.length" description="暂无数据"></el-empty>
|
||||
|
||||
<!-- 列表内容 -->
|
||||
<template v-else>
|
||||
<div class="list-header">
|
||||
<div class="header-item" style="width: 50px">#</div>
|
||||
<div class="header-item" style="width: 180px">订单编号</div>
|
||||
<div class="header-item" style="width: 100px">使用用户</div>
|
||||
<div class="header-item" style="width: 120px">手机号</div>
|
||||
<div class="header-item" style="width: 80px">状态</div>
|
||||
<div class="header-item" style="width: 90px">支付费用</div>
|
||||
<div class="header-item" style="flex: 1">下单时间</div>
|
||||
</div>
|
||||
|
||||
<pagination
|
||||
:auto-scroll="false"
|
||||
v-show="total>0"
|
||||
:total="total"
|
||||
:page.sync="queryParams.pageNum"
|
||||
:limit.sync="queryParams.pageSize"
|
||||
@pagination="getOrderList"
|
||||
/>
|
||||
<div class="list-content">
|
||||
<div v-for="(item, index) in orderList"
|
||||
:key="item.orderNo"
|
||||
class="list-item"
|
||||
:class="{ 'is-even': index % 2 === 1 }">
|
||||
<div class="item-col" style="width: 50px">{{ (queryParams.pageNum - 1) * queryParams.pageSize + index + 1 }}</div>
|
||||
<div class="item-col" style="width: 180px">
|
||||
<router-link
|
||||
:to="'/system/order/index/' + item.orderNo"
|
||||
class="link-type">
|
||||
{{ item.orderNo }}
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="item-col" style="width: 100px">{{ item.userName || '-' }}</div>
|
||||
<div class="item-col" style="width: 120px">{{ item.phonenumber || '-' }}</div>
|
||||
<div class="item-col" style="width: 80px">
|
||||
<dict-tag
|
||||
:options="dict.type.et_order_status"
|
||||
:value="item.status"/>
|
||||
</div>
|
||||
<div class="item-col amount" style="width: 90px">{{ item.totalFee }} 元</div>
|
||||
<div class="item-col" style="flex: 1">{{ parseTime(item.createTime) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 分页器 -->
|
||||
<div class="pagination-wrapper">
|
||||
<pagination
|
||||
v-show="total > 0"
|
||||
:total="total"
|
||||
:page.sync="queryParams.pageNum"
|
||||
:limit.sync="queryParams.pageSize"
|
||||
@pagination="getOrderList"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {listOrder} from "@/api/system/order";
|
||||
import { listOrder } from "@/api/system/order";
|
||||
import { parseTime } from "@/utils/ruoyi";
|
||||
|
||||
export default {
|
||||
name: 'orderRecord',
|
||||
name: 'OrderRecord',
|
||||
dicts: ['et_order_status'],
|
||||
|
||||
props: {
|
||||
// 设备id
|
||||
sn: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
orderList: [], // 充值记录列表
|
||||
orderList: [],
|
||||
loading: false,
|
||||
total: 0,
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 20,
|
||||
pageSize: 10,
|
||||
type: 1,
|
||||
sn: null,
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
sn(nv, ov) {
|
||||
this.getOrderList(nv);
|
||||
sn: {
|
||||
handler(newVal) {
|
||||
if (newVal) {
|
||||
this.getOrderList(newVal);
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getOrderList(this.sn);
|
||||
},
|
||||
|
||||
methods: {
|
||||
// 获取充值记录
|
||||
parseTime,
|
||||
|
||||
getOrderList(sn) {
|
||||
if(sn == null) {
|
||||
this.recordList = [];
|
||||
console.log(sn,'this.queryParams');
|
||||
if (!sn) {
|
||||
this.orderList = [];
|
||||
this.total = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
this.queryParams.sn = sn | this.sn;
|
||||
listOrder(this.queryParams).then(response => {
|
||||
this.orderList = response.rows;
|
||||
this.total = response.total;
|
||||
}).finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
closeAllDialogs() {
|
||||
this.$eventBus.$emit('close-all-dialogs');
|
||||
if(sn.page){
|
||||
listOrder(this.queryParams)
|
||||
.then(response => {
|
||||
this.orderList = response.rows;
|
||||
this.total = response.total;
|
||||
})
|
||||
.catch(() => {
|
||||
this.orderList = [];
|
||||
this.total = 0;
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
}else{
|
||||
this.queryParams.sn = sn;
|
||||
listOrder(this.queryParams)
|
||||
.then(response => {
|
||||
this.orderList = response.rows;
|
||||
this.total = response.total;
|
||||
})
|
||||
.catch(() => {
|
||||
this.orderList = [];
|
||||
this.total = 0;
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.order-record-container {
|
||||
background-color: #fff;
|
||||
|
||||
.record-list {
|
||||
min-height: 300px;
|
||||
position: relative;
|
||||
|
||||
.list-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 50px;
|
||||
background-color: #f5f7fa;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-bottom: none;
|
||||
|
||||
.header-item {
|
||||
padding: 0 15px;
|
||||
font-weight: 500;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
line-height: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
.list-content {
|
||||
border: 1px solid #e4e7ed;
|
||||
border-bottom: none;
|
||||
|
||||
.list-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 50px;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
transition: background-color 0.3s;
|
||||
|
||||
&.is-even {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.item-col {
|
||||
padding: 10px 15px;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
&.amount {
|
||||
color: #f56c6c;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.link-type {
|
||||
color: #409EFF;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
color: #66b1ff;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-wrapper {
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-top: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 暗色主题
|
||||
.dark {
|
||||
.order-record-container {
|
||||
background-color: #1f1f1f;
|
||||
|
||||
.record-list {
|
||||
.list-header {
|
||||
background-color: #2d2d2d;
|
||||
border-color: #434343;
|
||||
|
||||
.header-item {
|
||||
color: #a6a6a6;
|
||||
}
|
||||
}
|
||||
|
||||
.list-content {
|
||||
border-color: #434343;
|
||||
|
||||
.list-item {
|
||||
border-bottom-color: #434343;
|
||||
|
||||
&.is-even {
|
||||
background-color: #2a2a2a;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #2d2d2d;
|
||||
}
|
||||
|
||||
.item-col {
|
||||
color: #a6a6a6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-wrapper {
|
||||
border-color: #434343;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式处理
|
||||
@media screen and (max-width: 768px) {
|
||||
.order-record-container {
|
||||
.record-list {
|
||||
.list-header,
|
||||
.list-item {
|
||||
.header-item,
|
||||
.item-col {
|
||||
padding: 10px 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
1160
src/views/system/device/device_detail.vue
Normal file
1160
src/views/system/device/device_detail.vue
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div class="app-container">
|
||||
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
|
||||
<el-form-item label="mac" prop="mac">
|
||||
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px" v-if="type != 'device'">
|
||||
<el-form-item label="mac" prop="mac" >
|
||||
<el-input
|
||||
v-model="queryParams.mac"
|
||||
placeholder="请输入mac"
|
||||
|
@ -209,6 +209,7 @@ import { listLocationLog, getLocationLog, delLocationLog, addLocationLog, update
|
|||
export default {
|
||||
name: "LocationLog",
|
||||
dicts: ["et_device_lock_status","as_device_status"],
|
||||
props:['mac','type'],
|
||||
data() {
|
||||
return {
|
||||
// 遮罩层
|
||||
|
@ -244,11 +245,17 @@ export default {
|
|||
};
|
||||
},
|
||||
created() {
|
||||
const mac = this.$route.params && this.$route.params.mac;
|
||||
if(this.type=='device'){
|
||||
this.queryParams.mac = this.mac;
|
||||
this.getList();
|
||||
}else{
|
||||
const mac = this.$route.params && this.$route.params.mac;
|
||||
if (mac != null) {
|
||||
this.queryParams.mac = mac;
|
||||
}
|
||||
this.getList();
|
||||
}
|
||||
|
||||
},
|
||||
methods: {
|
||||
// 格式化电动车状态
|
||||
|
|
|
@ -140,7 +140,7 @@
|
|||
<el-input v-model="form.model" placeholder="请输入车型" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="userName == 'admin'" label="代理商" prop="operator">
|
||||
<el-select v-model="form.operator" style="width: 100%" clearable placeholder="请选择代理商">
|
||||
<el-select v-model="form.operator" style="width: 100%" filterable clearable placeholder="请选择代理商">
|
||||
<el-option
|
||||
v-for="item in deptOptions"
|
||||
:key="item.deptId"
|
||||
|
@ -149,8 +149,8 @@
|
|||
></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="运营区" prop="areaId">
|
||||
<el-select v-model="form.areaId" style="width: 100%" clearable placeholder="请选择代理商">
|
||||
<!-- <el-form-item label="运营区" prop="areaId">
|
||||
<el-select v-model="form.areaId" style="width: 100%" clearable placeholder="请选择代理商">
|
||||
<el-option
|
||||
v-for="item in areaOptions"
|
||||
:key="item.areaId"
|
||||
|
@ -158,7 +158,7 @@
|
|||
:value="item.areaId"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form-item> -->
|
||||
<el-form-item label="满电电压(V)" label-width="90" prop="fullVoltage">
|
||||
<el-input style="width: 85%" v-model="form.fullVoltage" placeholder="请输入满电电压" />
|
||||
</el-form-item>
|
||||
|
@ -290,9 +290,15 @@ export default {
|
|||
};
|
||||
},
|
||||
created() {
|
||||
const model = this.$route.query.model
|
||||
if(model){
|
||||
this.queryParams.model = model
|
||||
}
|
||||
this.getList();
|
||||
console.log("当前用户信息:",this.$store.state.user.name)
|
||||
this.userName = this.$store.state.user.name;
|
||||
|
||||
|
||||
if(this.userName == 'admin'){
|
||||
this.getDeptTree();
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user