Compare commits

...

6 Commits

Author SHA1 Message Date
d2f2922152 Merge branch 'refs/heads/tx' 2024-12-26 22:12:03 +08:00
tx
fcaba878f5 设备详情 2024-12-25 09:56:10 +08:00
tx
7576060097 轨迹 2024-12-24 08:47:57 +08:00
tx
56b3b0f438 bug修改 2024-12-20 16:25:49 +08:00
tx
005bd0ef63 代码优化 2024-12-13 15:44:30 +08:00
tx
5ca920b185 车辆类别修改优化 2024-12-13 15:10:35 +08:00
14 changed files with 3156 additions and 1032 deletions

View File

@ -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

View File

@ -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",

View File

@ -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({

View 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>

View File

@ -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,

View File

@ -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'){

View File

@ -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() {

View File

@ -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, // y0
min: 0,
axisLine: {
lineStyle: {
color: '#5470C6'
@ -98,7 +137,7 @@ export default {
type: 'value',
name: '订单',
position: 'right',
min: 0, // y0
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>

View File

@ -232,7 +232,7 @@
},
{
label: '仓库',
offlineCount: (vo.allOfflineNum || 0) - (offline.offlineNum || 0),
offlineCount: (vo.allOfflineNum || 0) - (vo.offlineNum || 0),
totalCount: vo.inStashNum || 0
},
{

View File

@ -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>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -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: {
//

View File

@ -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();
}