From 4f254348b82a9d73ba530256d3e3c3174ff3d385 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A3=B7=E5=8F=B6?= <14103883+leaf-phos@user.noreply.gitee.com> Date: Sat, 26 Apr 2025 18:41:39 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main.js | 9 + src/utils/date.js | 28 - .../components/AreaJoinEditDialog.vue | 8 +- .../bst/areaSub/components/AddressSearch.vue | 212 ++++ src/views/bst/areaSub/components/AreaMap.js | 128 ++- src/views/bst/areaSub/components/AreaMap.vue | 918 +----------------- .../areaSub/components/mixins/LocationLog.js | 347 +++++++ .../components/mixins/OperationArea.js | 87 ++ .../bst/areaSub/components/mixins/SubArea.js | 335 +++++++ src/views/bst/device/index.vue | 74 +- .../device/view/components/DeviceLocation.vue | 156 ++- src/views/bst/order/util.js | 15 + src/views/bst/order/view/view.vue | 8 +- 13 files changed, 1310 insertions(+), 1015 deletions(-) create mode 100644 src/views/bst/areaSub/components/AddressSearch.vue create mode 100644 src/views/bst/areaSub/components/mixins/LocationLog.js create mode 100644 src/views/bst/areaSub/components/mixins/OperationArea.js create mode 100644 src/views/bst/areaSub/components/mixins/SubArea.js create mode 100644 src/views/bst/order/util.js diff --git a/src/main.js b/src/main.js index ee24aed..0d661ac 100644 --- a/src/main.js +++ b/src/main.js @@ -8,6 +8,7 @@ import './assets/styles/element-variables.scss'; import '@/assets/styles/index.scss'; // global css import '@/assets/styles/ipad.scss'; // ipad global css import '@/assets/styles/ruoyi.scss'; // ruoyi css +import globalConfig from '@/utils/config/globalConfig'; import { download } from '@/utils/request'; import App from './App'; import directive from './directive'; // directive @@ -99,6 +100,14 @@ new Vue({ }) + +// 高德地图 +window._AMapSecurityConfig = { + securityJsCode: globalConfig.aMap.secret, +}; + + + // 全局添加table左右拖动效果的指令 Vue.directive('tableMove', { bind: function (el, binding, vnode) { diff --git a/src/utils/date.js b/src/utils/date.js index 1a45c4f..191aba6 100644 --- a/src/utils/date.js +++ b/src/utils/date.js @@ -1,31 +1,5 @@ import { parseTime } from '@/utils/ruoyi'; -/** - * 计算周年 - */ -export function calcFullYear(date) { - if (date == null ) { - return 0; - } - let start = toDate(date); - let end = new Date(); - - return Math.floor((end.getTime() - start.getTime()) / 31536000000) -} - -/** - * 计算年龄 - */ -export function calcBirthDay(birthday) { - if (birthday === null) { - return 0; - } - let now = new Date(); - let start = toDate(birthday); - - return now.getFullYear() - start.getFullYear() + 1; -} - /** * 转为日期 */ @@ -49,8 +23,6 @@ export function calcSecond(start, end) { return Math.floor((endDate.getTime() - startDate.getTime()) / 1000); } - - // 获取前n天的日期 export function getLastDate(n) { let now = new Date(); diff --git a/src/views/bst/areaJoin/components/AreaJoinEditDialog.vue b/src/views/bst/areaJoin/components/AreaJoinEditDialog.vue index b4bf172..d43a42a 100644 --- a/src/views/bst/areaJoin/components/AreaJoinEditDialog.vue +++ b/src/views/bst/areaJoin/components/AreaJoinEditDialog.vue @@ -43,7 +43,7 @@ @@ -83,6 +83,7 @@ export default { AreaJoinType, span: 24, title: '', + submitLoading: false, form: { permissions: [] }, @@ -154,12 +155,15 @@ export default { submitForm() { this.$refs.form.validate(valid => { if (valid) { + this.submitLoading = true; const promise = this.form.id != null ? updateAreaJoin(this.form) : addAreaJoin(this.form); promise.then(response => { this.$modal.msgSuccess(this.form.id != null ? "修改成功" : "新增成功"); this.dialogVisible = false; this.$emit('success'); - }); + }).finally(() => { + this.submitLoading = false; + }) } }); }, diff --git a/src/views/bst/areaSub/components/AddressSearch.vue b/src/views/bst/areaSub/components/AddressSearch.vue new file mode 100644 index 0000000..1dd8d93 --- /dev/null +++ b/src/views/bst/areaSub/components/AddressSearch.vue @@ -0,0 +1,212 @@ + + + + + \ No newline at end of file diff --git a/src/views/bst/areaSub/components/AreaMap.js b/src/views/bst/areaSub/components/AreaMap.js index dcb3910..ecf14fa 100644 --- a/src/views/bst/areaSub/components/AreaMap.js +++ b/src/views/bst/areaSub/components/AreaMap.js @@ -60,6 +60,34 @@ export const LABEL_STYLES = { } }; +// 设备图标配置 +const icons = { + disabled_offline: 'https://lxnapi.ccttiot.com/bike/img/static/ucBKG3ebYRAToVweJihu', // 禁用(离线) + disabled: "https://lxnapi.ccttiot.com/bike/img/static/uyK7Vg4Lu8xb3oNVuG2l", // 禁用 + dispatching_offline: "https://lxnapi.ccttiot.com/bike/img/static/uhZudZM3nEKj0tYKlho2", // 调度中(离线) + dispatching: "https://lxnapi.ccttiot.com/bike/img/static/ujur6TezvPf4buFAqPHo", // 调度中 + temp_lock_offline: "https://lxnapi.ccttiot.com/bike/img/static/uRod2zf3t9dAOYafWoWt", // 临时锁车(离线) + temp_lock: "https://lxnapi.ccttiot.com/bike/img/static/uZpXq3TBtM5gVgJJeImY", // 临时锁车 + in_use_offline: "https://lxnapi.ccttiot.com/bike/img/static/uG13E7BpUFF44wVYC9no", // 骑行中(离线) + in_use: "https://lxnapi.ccttiot.com/bike/img/static/uHQIdWCTmtUztl49wBKU", // 骑行中 + reserved_offline: "https://lxnapi.ccttiot.com/bike/img/static/uR3DQEssiK62ovhh88y8", // 预约中(离线) + reserved: "https://lxnapi.ccttiot.com/bike/img/static/u460R1NKWHEpHbt0U4H7", // 预约中 + available_offline: "https://lxnapi.ccttiot.com/bike/img/static/uzhMeExOQJbMcZtrfGUV", // 待骑行(离线) + available: "https://lxnapi.ccttiot.com/bike/img/static/uheL17wVZn24BwCwEztT", // 待骑行 + storage_offline: "https://lxnapi.ccttiot.com/bike/img/static/uQRng4QNKA38Amk8Wgt5", // 仓库中(离线) + storage: "https://lxnapi.ccttiot.com/bike/img/static/uocjFo8Ar2BJVpzC2G2f", // 仓库中 +}; + +// 设备图标映射 +export const DEVICE_STATUS_ICONS = { + [DeviceStatus.STORAGE]: icons.storage, // 仓库中 + [DeviceStatus.AVAILABLE]: icons.available, // 待骑行 + [DeviceStatus.IN_USE]: icons.in_use, // 骑行中 + [DeviceStatus.TEMP_LOCKED]: icons.temp_lock, // 临时锁车 + [DeviceStatus.DISPATCHING]: icons.dispatching,// 调度中 + [DeviceStatus.DISABLED]: icons.disabled, // 禁用 + [DeviceStatus.Q_LOCKED]: icons.temp_lock, // 强制锁车 +}; // SN标签样式 export function getSnLabel(AMap, sn, position) { @@ -82,30 +110,80 @@ export function getSnLabel(AMap, sn, position) { }); } -const icons = { - disabled_offline: 'https://lxnapi.ccttiot.com/bike/img/static/ucBKG3ebYRAToVweJihu', // 禁用(离线) - disabled: "https://lxnapi.ccttiot.com/bike/img/static/uyK7Vg4Lu8xb3oNVuG2l", // 禁用 - dispatching_offline: "https://lxnapi.ccttiot.com/bike/img/static/uhZudZM3nEKj0tYKlho2", // 调度中(离线) - dispatching: "https://lxnapi.ccttiot.com/bike/img/static/ujur6TezvPf4buFAqPHo", // 调度中 - temp_lock_offline: "https://lxnapi.ccttiot.com/bike/img/static/uRod2zf3t9dAOYafWoWt", // 临时锁车(离线) - temp_lock: "https://lxnapi.ccttiot.com/bike/img/static/uZpXq3TBtM5gVgJJeImY", // 临时锁车 - in_use_offline: "https://lxnapi.ccttiot.com/bike/img/static/uG13E7BpUFF44wVYC9no", // 骑行中(离线) - in_use: "https://lxnapi.ccttiot.com/bike/img/static/uHQIdWCTmtUztl49wBKU", // 骑行中 - reserved_offline: "https://lxnapi.ccttiot.com/bike/img/static/uR3DQEssiK62ovhh88y8", // 预约中(离线) - reserved: "https://lxnapi.ccttiot.com/bike/img/static/u460R1NKWHEpHbt0U4H7", // 预约中 - available_offline: "https://lxnapi.ccttiot.com/bike/img/static/uzhMeExOQJbMcZtrfGUV", // 待骑行(离线) - available: "https://lxnapi.ccttiot.com/bike/img/static/uheL17wVZn24BwCwEztT", // 待骑行 - storage_offline: "https://lxnapi.ccttiot.com/bike/img/static/uQRng4QNKA38Amk8Wgt5", // 仓库中(离线) - storage: "https://lxnapi.ccttiot.com/bike/img/static/uocjFo8Ar2BJVpzC2G2f", // 仓库中 -} +// 公共方法 +export const mapMethods = { + // 开始绘制(不管是那种边界) + startEditArea() { + this.isEditing = true; -// 设备图标映射 -export const DEVICE_STATUS_ICONS = { - [DeviceStatus.STORAGE]: icons.storage, // 仓库中 - [DeviceStatus.AVAILABLE]: icons.available, // 待骑行 - [DeviceStatus.IN_USE]: icons.in_use, // 骑行中 - [DeviceStatus.TEMP_LOCKED]: icons.temp_lock, // 临时锁车 - [DeviceStatus.DISPATCHING]: icons.dispatching,// 调度中 - [DeviceStatus.DISABLED]: icons.disabled, // 禁用 - [DeviceStatus.Q_LOCKED]: icons.temp_lock, // 强制锁车 + // 关闭当前信息窗口 + if (this.currentInfoWindow) { + this.currentInfoWindow.close(); + } + }, + // 清理方法优化 + clearOverlays(overlayMap) { + if (overlayMap == null || overlayMap.size == 0) { + return; + } + overlayMap.forEach(overlay => { + this.map.remove(overlay); + }); + overlayMap.clear(); + }, + // 获取位置 + getPosition(item) { + if (item == null) { + return null; + } + if (item.longitude == null || item.latitude == null) { + return null; + } + return [item.longitude, item.latitude]; + }, + + // 聚焦 + focus(polygon, position) { + if (polygon) { + const bounds = polygon.getBounds(); + const padding = [150, 300, 150, 300]; + this.map.setBounds(bounds, false, padding); + } else if (position) { + this.map.setZoomAndCenter(15, position, false, 300); + } + }, + + // 计算多边形中心点 + calculateCenter(path) { + if (!path || path.length === 0) return null; + + let total = path.length; + let X = 0, Y = 0, Z = 0; + + path.forEach(point => { + let lng = (point[0] * Math.PI) / 180; + let lat = (point[1] * Math.PI) / 180; + + let x = Math.cos(lat) * Math.cos(lng); + let y = Math.cos(lat) * Math.sin(lng); + let z = Math.sin(lat); + + X += x; + Y += y; + Z += z; + }); + + X = X / total; + Y = Y / total; + Z = Z / total; + + let Lng = Math.atan2(Y, X); + let Hyp = Math.sqrt(X * X + Y * Y); + let Lat = Math.atan2(Z, Hyp); + + return [ + (Lng * 180) / Math.PI, + (Lat * 180) / Math.PI + ]; + } }; diff --git a/src/views/bst/areaSub/components/AreaMap.vue b/src/views/bst/areaSub/components/AreaMap.vue index aeb4ae7..1cc5412 100644 --- a/src/views/bst/areaSub/components/AreaMap.vue +++ b/src/views/bst/areaSub/components/AreaMap.vue @@ -3,6 +3,9 @@
+ + +
@@ -10,9 +13,8 @@ 电子围栏 全局查看 切换样式 - {{ showLabels ? '隐藏' : '显示' }}文字 + {{ showLabels ? '隐藏' : '显示' }}标签 {{ isPlaybackVisible ? '隐藏' : '显示' }}轨迹控制台 - {{ isDebugMode ? '退出调试' : '调试' }}
@@ -50,18 +52,21 @@ diff --git a/src/views/bst/areaSub/components/mixins/LocationLog.js b/src/views/bst/areaSub/components/mixins/LocationLog.js new file mode 100644 index 0000000..2212f6f --- /dev/null +++ b/src/views/bst/areaSub/components/mixins/LocationLog.js @@ -0,0 +1,347 @@ +import { isEmpty } from "@/utils/index"; +import { DEVICE_STATUS_ICONS, getSnLabel } from "@/views/bst/areaSub/components/AreaMap"; + +export default { + data() { + return { + isPlaybackVisible: false, + currentLogIndex: 0, + currentSpeed: 0, + isPlaying: false, + playbackSpeed: 1, + playbackTimer: null, + currentPolylines: [], // 修改为数组,存储多段轨迹 + currentDeviceMarker: null, + currentDeviceIcon: null, // 新增:存储当前设备图标对象 + currentLog: null, + sortedLocationLogs: [], + progressPolylines: [], // 修改为数组,存储多段进度轨迹 + deviceSegments: [], // 新增:存储按设备ID分段的轨迹数据 + }; + }, + + watch: { + // 监听播放状态 + isPlaying(newVal) { + if (newVal) { + this.startPlayback(); + } else { + this.pausePlayback(); + } + } + }, + + methods: { + // 初始化轨迹播放 + initPlayback() { + if (!this.map) { + return; + } + // 清除现有轨迹和设备标记 + if (this.currentPolylines.length > 0) { + this.currentPolylines.forEach(polyline => { + this.map.remove(polyline); + }); + this.currentPolylines = []; + } + if (this.progressPolylines.length > 0) { + this.progressPolylines.forEach(polyline => { + this.map.remove(polyline); + }); + this.progressPolylines = []; + } + if (this.currentDeviceMarker) { + if (this.currentDeviceMarker.snLabel) { + this.map.remove(this.currentDeviceMarker.snLabel); + } + this.map.remove(this.currentDeviceMarker); + this.currentDeviceMarker = null; + } + + if (isEmpty(this.locationLogList)) { + this.sortedLocationLogs = []; + this.currentLogIndex = 0; + this.currentLog = null; + return; + } else { + this.currentLogIndex = this.locationLogList.length - 1; + } + + // 按时间排序日志并预计算速度 + this.sortedLocationLogs = [...this.locationLogList] + .sort((a, b) => new Date(a.at).getTime() - new Date(b.at).getTime()) + .map((log, index, array) => { + if (index === 0) { + log.speed = 0; + } else { + const prevLog = array[index - 1]; + const prevTime = new Date(prevLog.at).getTime(); + const currentTime = new Date(log.at).getTime(); + const timeDiff = (currentTime - prevTime) / 1000; // 转换为秒 + + if (timeDiff === 0) { + log.speed = 0; + } else { + const distance = this.calculateDistance( + prevLog.latitude, prevLog.longitude, + log.latitude, log.longitude + ); + log.speed = Number(((distance / timeDiff) * 3.6).toFixed(1)); // 转换为km/h并保留一位小数 + } + } + return log; + }); + + this.updateCurrentLog(); + this.drawTrajectory(); + }, + + // 计算两点之间的距离 + calculateDistance(lat1, lon1, lat2, lon2) { + if (this.AMap) { + const start = new this.AMap.LngLat(lon1, lat1); + const end = new this.AMap.LngLat(lon2, lat2); + return start.distance(end); + } + return 0; + }, + + // 更新当前日志 + updateCurrentLog() { + if (this.sortedLocationLogs.length === 0) { + return; + } + + this.currentLog = this.sortedLocationLogs[this.currentLogIndex]; + this.updateDeviceMarker(); + }, + + // 更新设备标记 + updateDeviceMarker() { + if (!this.currentLog || !this.map) { + return; + } + + const position = [this.currentLog.longitude, this.currentLog.latitude]; + + if (!this.currentDeviceMarker) { + // 创建图标对象 + this.currentDeviceIcon = new this.AMap.Icon({ + image: DEVICE_STATUS_ICONS[this.currentLog.status], + size: new this.AMap.Size(32, 36), + imageSize: new this.AMap.Size(32, 36), + anchor: 'bottom-center' + }); + + // 首次创建设备标记 + this.currentDeviceMarker = new this.AMap.Marker({ + position: position, + offset: new this.AMap.Pixel(-16, -36), + icon: this.currentDeviceIcon + }); + + // 保存当前设备ID + this.currentDeviceMarker.deviceId = this.currentLog.deviceId; + + // 创建SN标签 + const snLabel = getSnLabel(this.AMap, this.currentLog.sn, position); + + this.map.add([this.currentDeviceMarker, snLabel]); + // 保存snLabel引用 + this.currentDeviceMarker.snLabel = snLabel; + } else { + // 更新位置 + this.currentDeviceMarker.setPosition(position); + + // 只有当状态发生变化时才更新图标 + if (this.currentDeviceIcon.getImage() !== DEVICE_STATUS_ICONS[this.currentLog.status]) { + this.currentDeviceIcon.setImage(DEVICE_STATUS_ICONS[this.currentLog.status]); + this.currentDeviceMarker.setIcon(this.currentDeviceIcon); + } + + // 更新设备ID + this.currentDeviceMarker.deviceId = this.currentLog.deviceId; + + // 更新SN标签位置和内容 + if (this.currentDeviceMarker.snLabel) { + this.currentDeviceMarker.snLabel.setPosition(position); + // 只有当SN发生变化时才更新文本 + if (this.currentDeviceMarker.snLabel.getText() !== this.currentLog.sn) { + this.currentDeviceMarker.snLabel.setText(this.currentLog.sn); + } + } + } + + // 更新进度轨迹 + this.updateProgressTrajectory(); + }, + + // 绘制轨迹 + drawTrajectory() { + // 清除现有轨迹 + this.currentPolylines.forEach(polyline => { + this.map.remove(polyline); + }); + this.progressPolylines.forEach(polyline => { + this.map.remove(polyline); + }); + this.currentPolylines = []; + this.progressPolylines = []; + + // 按设备ID分段 + let currentDeviceId = null; + let currentSegment = []; + this.deviceSegments = []; + + this.sortedLocationLogs.forEach(log => { + if (currentDeviceId !== log.deviceId) { + if (currentSegment.length > 0) { + this.deviceSegments.push(currentSegment); + } + currentSegment = [log]; + currentDeviceId = log.deviceId; + } else { + currentSegment.push(log); + } + }); + + // 添加最后一段 + if (currentSegment.length > 0) { + this.deviceSegments.push(currentSegment); + } + + // 为每个段落创建轨迹线 + this.deviceSegments.forEach(segment => { + const path = segment.map(log => [log.longitude, log.latitude]); + + // 创建完整轨迹 + const polyline = new this.AMap.Polyline({ + path: path, + strokeColor: '#409EFF', + strokeWeight: 6, + strokeOpacity: 0.4, + showDir: true + }); + + // 创建进度轨迹 + const progressPolyline = new this.AMap.Polyline({ + path: path, + strokeColor: '#67C23A', + strokeWeight: 6, + strokeOpacity: 0.8, + showDir: true + }); + + this.currentPolylines.push(polyline); + this.progressPolylines.push(progressPolyline); + this.map.add([polyline, progressPolyline]); + }); + + // 调整地图视野以显示所有轨迹 + if (this.currentPolylines.length > 0) { + this.map.setFitView(this.currentPolylines); + } + }, + + // 更新轨迹进度条 + updateProgressTrajectory() { + if (this.currentLogIndex < 0) return; + + // 找到当前日志所在的段落 + let currentSegmentIndex = -1; + let accumulatedCount = 0; + + for (let i = 0; i < this.deviceSegments.length; i++) { + accumulatedCount += this.deviceSegments[i].length; + if (this.currentLogIndex < accumulatedCount) { + currentSegmentIndex = i; + break; + } + } + + if (currentSegmentIndex === -1) { + return; + } + + // 更新所有进度轨迹 + this.progressPolylines.forEach((progressPolyline, index) => { + if (index < currentSegmentIndex) { + // 之前的段落完全显示 + const segmentPath = this.deviceSegments[index].map(log => [log.longitude, log.latitude]); + progressPolyline.setPath(segmentPath); + progressPolyline.show(); + progressPolyline.setOptions({ + strokeOpacity: 0.8 + }); + } else if (index === currentSegmentIndex) { + // 当前段落显示到当前位置 + const segmentStartIndex = index === 0 ? 0 : this.deviceSegments.slice(0, index).reduce((sum, seg) => sum + seg.length, 0); + const currentSegmentPosition = this.currentLogIndex - segmentStartIndex; + const progressPath = this.deviceSegments[index] + .slice(0, currentSegmentPosition + 1) + .map(log => [log.longitude, log.latitude]); + progressPolyline.setPath(progressPath); + progressPolyline.show(); + progressPolyline.setOptions({ + strokeOpacity: 0.8 + }); + } else { + // 后面的段落设置为不可见 + const segmentPath = this.deviceSegments[index].map(log => [log.longitude, log.latitude]); + progressPolyline.setPath(segmentPath); + progressPolyline.setOptions({ + strokeOpacity: 0 + }); + } + }); + }, + + // 播放控制相关方法 + togglePlaybackPanel() { + this.isPlaybackVisible = !this.isPlaybackVisible; + }, + + handleSliderChange(index) { + let oldIndex = this.currentLogIndex; + this.currentLogIndex = index; + if (oldIndex !== index) { + this.updateCurrentLog(); + } + }, + + togglePlay() { + this.isPlaying = !this.isPlaying; + }, + + setPlaybackSpeed(speed) { + this.playbackSpeed = speed; + // 如果正在播放,立即应用新的速度 + if (this.isPlaying) { + this.pausePlayback(); + this.startPlayback(); + } + }, + + startPlayback() { + if (this.playbackTimer) { + clearInterval(this.playbackTimer); + } + + this.playbackTimer = setInterval(() => { + if (this.currentLogIndex >= this.sortedLocationLogs.length - 1) { + this.currentLogIndex = 0; + } else { + this.currentLogIndex++; + } + this.updateCurrentLog(); + }, 1000 / this.playbackSpeed); + }, + + pausePlayback() { + if (this.playbackTimer) { + clearInterval(this.playbackTimer); + this.playbackTimer = null; + } + } + } +}; \ No newline at end of file diff --git a/src/views/bst/areaSub/components/mixins/OperationArea.js b/src/views/bst/areaSub/components/mixins/OperationArea.js new file mode 100644 index 0000000..42fab1b --- /dev/null +++ b/src/views/bst/areaSub/components/mixins/OperationArea.js @@ -0,0 +1,87 @@ + + +import { debounce } from "@/utils/index"; +export default { + data() { + return { + areaPolygons: new Map(), + } + }, + watch: { + // 监听区域数据变化 + area: { + handler: debounce(function() { + this.renderAll(); + }, 300), + deep: true + }, + }, + methods: { + // 渲染运营区边界 + renderAreaBoundary() { + this.clearOverlays(this.areaPolygons); + + if (!this.map || !this.area.boundaryStr) { + return; + }; + + try { + const boundary = JSON.parse(this.area.boundaryStr); + const polygon = new this.AMap.Polygon({ + path: boundary, + strokeColor: '#2b8cbe', + strokeWeight: 2, + fillColor: '#ccebc5', + fillOpacity: 0.3, + strokeStyle: 'dashed', + strokeDasharray: [5, 5] + }); + + this.map.add(polygon); + this.areaPolygons.set(this.area.id, polygon); + } catch (error) { + console.error('渲染运营区边界失败:', error); + } + }, + + // 开始编辑运营区边界 + startAreaBoundaryEdit() { + this.clearEditor(); + this.focusArea(this.area); + + if (this.area.boundaryStr) { + try { + const boundary = JSON.parse(this.area.boundaryStr); + this.editingPolygon = new this.AMap.Polygon({ + path: boundary, + strokeColor: '#2b8cbe', + strokeWeight: 2, + fillColor: '#ccebc5', + fillOpacity: 0.3, + strokeStyle: 'dashed', + strokeDasharray: [5, 5] + }); + this.map.add(this.editingPolygon); + this.polygonEditor.setTarget(this.editingPolygon); + } catch (error) { + console.error('加载运营区边界数据失败:', error); + this.$message.error('加载运营区边界数据失败'); + } + } + + this.polygonEditor.open(); + this.isEditingArea = true; + this.startEditArea(); + }, + + // 聚焦运营区 + focusArea(area) { + if (area == null) { + return; + } + const position = this.getPosition(area); + const polygon = this.areaPolygons.get(area.id); + this.focus(polygon, position); + } + } +}; \ No newline at end of file diff --git a/src/views/bst/areaSub/components/mixins/SubArea.js b/src/views/bst/areaSub/components/mixins/SubArea.js new file mode 100644 index 0000000..834eac4 --- /dev/null +++ b/src/views/bst/areaSub/components/mixins/SubArea.js @@ -0,0 +1,335 @@ + +import { AreaSubStatus } from "@/utils/enums"; +import { debounce } from "@/utils/index"; +import { AREA_STYLES, HIGHLIGHT_STYLES, LABEL_STYLES, MARKER_ICONS } from "@/views/bst/areaSub/components/AreaMap"; + +export default { + data() { + return { + highlightedAreaId: null, // 当前鼠标悬停的区域ID + selectedAreaId: null, // 当前选中的区域ID + editSubArea: null, + currentInfoWindow: null, + subAreaPolygons: new Map(), + subAreaMarkers: new Map(), + subAreaLabels: new Map(), + }; + }, + + watch: { + // 监听子区域列表变化 + areaSubList: { + handler: debounce(function() { + this.renderAll(); + }, 300), + deep: true + }, + // 监听高亮状态变化 + highlightedAreaId: { + handler(newVal, oldVal) { + if (oldVal) { + this.updateAreaStyle(oldVal); + } + if (newVal) { + this.updateAreaStyle(newVal); + } + } + }, + // 监听选中状态变化 + selectedAreaId: { + handler(newVal, oldVal) { + if (oldVal) { + this.updateAreaStyle(oldVal); + } + if (newVal) { + this.updateAreaStyle(newVal); + } + } + } + }, + + methods: { + initSubArea() { + // 添加地图点击事件,关闭信息窗体和清除选中状态 + this.map.on('click', () => { + if (this.currentInfoWindow) { + this.currentInfoWindow.close(); + } + if (!this.isEditing) { + this.selectedAreaId = null; + } + }); + this.renderSubAreas(); + }, + // 渲染子区域 + renderSubAreas() { + + this.clearHighlight(); + this.clearOverlays(this.subAreaPolygons); + this.clearOverlays(this.subAreaMarkers); + this.clearOverlays(this.subAreaLabels); + + if (!this.map || !this.areaSubList.length) { + return; + } + this.areaSubList.forEach(subArea => { + if (subArea.status === AreaSubStatus.DISABLED) { + return; + } + + try { + // 渲染边界 + if (subArea.boundaryStr) { + const boundary = JSON.parse(subArea.boundaryStr); + const polygon = new this.AMap.Polygon({ + path: boundary, + ...AREA_STYLES[subArea.type], + extData: { + id: subArea.id, + type: subArea.type + } + }); + + // 添加事件监听 + polygon.on('mouseover', () => { + if (!this.isEditing) { + this.highlightedAreaId = subArea.id; + } + }); + + polygon.on('mouseout', () => { + if (!this.isEditing) { + this.highlightedAreaId = null; + } + }); + + polygon.on('click', (e) => { + if (this.isDebugMode) { + // 在调试模式下,不阻止事件冒泡 + return; + } + if (!this.isEditing) { + // 如果点击的是当前选中的区域,不做任何操作 + if (this.selectedAreaId !== subArea.id) { + this.selectedAreaId = subArea.id; + this.showInfoWindow(subArea); + } + } + }); + + this.map.add(polygon); + this.subAreaPolygons.set(subArea.id, polygon); + } + + // 渲染标记点 + const marker = new this.AMap.Marker({ + position: [subArea.longitude, subArea.latitude], + icon: new this.AMap.Icon({ + image: MARKER_ICONS[subArea.type], + size: new this.AMap.Size(25, 36), + imageSize: new this.AMap.Size(25, 36) + }), + offset: new this.AMap.Pixel(-12.5, -36) + }); + + marker.on('click', (e) => { + if (this.isDebugMode) { + // 在调试模式下,不阻止事件冒泡 + return; + } + if (!this.isEditing) { + this.showInfoWindow(subArea); + } + }); + + this.map.add(marker); + this.subAreaMarkers.set(subArea.id, marker); + + // 渲染文字标签 + const label = new this.AMap.Text({ + text: subArea.name, + position: [subArea.longitude, subArea.latitude], + offset: new this.AMap.Pixel(0, -60), + anchor: 'center', + cursor: 'pointer', + style: { + ...LABEL_STYLES[subArea.type], + border: 'none', + fontSize: '12px', + padding: '2px 8px', + borderRadius: '4px', + whiteSpace: 'nowrap', + textAlign: 'center', + position: 'absolute', + transform: 'translateX(-50%)', + minWidth: '50px', + display: 'flex', + justifyContent: 'center', + alignItems: 'center' + } + }); + + label.on('click', (e) => { + if (this.isDebugMode) { + // 在调试模式下,不阻止事件冒泡 + return; + } + if (!this.isEditing) { + this.showInfoWindow(subArea); + } + }); + + this.map.add(label); + this.subAreaLabels.set(subArea.id, label); + label.setMap(this.showLabels ? this.map : null); + } catch (error) { + console.error(`渲染子区域失败 [${subArea.id}]:`, error); + } + }); + }, + // 切换标签显示 + toggleLabels() { + this.showLabels = !this.showLabels; + this.subAreaLabels.forEach(label => { + label.setMap(this.showLabels ? this.map : null); + }); + }, + // 开始边界编辑 + startBoundaryEdit(subArea, focus = false) { + this.clearEditor(); + this.editSubArea = subArea; + + if (focus) { + this.focusSubArea(subArea); + } + + if (subArea && subArea.boundaryStr) { + try { + const boundary = JSON.parse(subArea.boundaryStr); + this.editingPolygon = new this.AMap.Polygon({ + path: boundary, + ...AREA_STYLES[subArea.type] + }); + this.map.add(this.editingPolygon); + this.polygonEditor.setTarget(this.editingPolygon); + } catch (error) { + console.error('加载边界数据失败:', error); + this.$message.error('加载边界数据失败'); + } + } + this.polygonEditor.open(); + this.startEditArea(); + }, + // 显示信息窗体 + showInfoWindow(subArea, focus = false) { + if (!this.enableEdit) { + return; + } + if (this.currentInfoWindow) { + this.currentInfoWindow.close(); + } + + const position = this.getPosition(subArea); + + if (position == null) { + return; + } + + // 聚焦效果 + if (focus) { + this.focusSubArea(subArea); + } else { + this.map.setCenter(position); + } + + // 创建自定义内容,使用Vue的事件处理方式 + const content = document.createElement('div'); + content.className = 'map-info-window'; + content.innerHTML = ` +

${subArea.name}

+
+ 编辑信息 + 修改边界 + 删除 +
+ `; + + // 直接绑定事件 + content.addEventListener('click', (e) => { + const target = e.target; + if (target.classList.contains('info-action')) { + const action = target.getAttribute('data-action'); + this.handleInfoAction(action, subArea); + if (this.currentInfoWindow) { + this.currentInfoWindow.close(); + } + } + }); + + this.currentInfoWindow = new this.AMap.InfoWindow({ + content: content, + offset: new this.AMap.Pixel(0, -30), + closeWhenClickMap: true + }); + + // 添加关闭事件监听 + this.currentInfoWindow.on('close', () => { + if (!this.isEditing) { + this.selectedAreaId = null; + } + }); + + this.currentInfoWindow.open(this.map, position); + }, + + // 聚焦子区域 + focusSubArea(subArea) { + if (subArea == null) { + return; + } + let position = this.getPosition(subArea); + const polygon = this.subAreaPolygons.get(subArea.id); + this.focus(polygon, position); + }, + + // 处理信息窗体操作 + handleInfoAction(action, subArea) { + console.log("action", action); + switch (action) { + case 'edit': + this.$emit('edit', subArea); + break; + case 'boundary': + this.startBoundaryEdit(subArea, true); + break; + case 'delete': + this.$emit('delete', subArea); + break; + } + }, + + // 更新区域样式 + updateAreaStyle(areaId) { + const polygon = this.subAreaPolygons.get(areaId); + if (!polygon) return; + + const type = polygon.getExtData().type; + let style = { ...AREA_STYLES[type] }; + + // 根据状态叠加高亮样式 + if (areaId === this.selectedAreaId) { + style = { ...style, ...HIGHLIGHT_STYLES.selected }; + } else if (areaId === this.highlightedAreaId) { + style = { ...style, ...HIGHLIGHT_STYLES.hover }; + } + + polygon.setOptions(style); + }, + + // 清除所有高亮状态 + clearHighlight() { + this.highlightedAreaId = null; + this.selectedAreaId = null; + } + } +}; \ No newline at end of file diff --git a/src/views/bst/device/index.vue b/src/views/bst/device/index.vue index 97a7f9f..9112e64 100644 --- a/src/views/bst/device/index.vue +++ b/src/views/bst/device/index.vue @@ -17,22 +17,6 @@ @keyup.enter.native="handleQuery" /> - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + +
-
-
{{ log.at }}
-
- - - {{ log.longitude | dv }}, {{ log.latitude | dv }} - - - - {{ log.voltage }}V - - - {{ log.sn }} - - -
-
+ + + +
+
+
+ + {{ log.at }} +
+
+ + + {{ log.longitude.toFixed(8) }}, {{ log.latitude.toFixed(8) }} + + + + {{ log.voltage | fix2 }}V + + {{ log.sn }} +
+
+
+
+
@@ -84,6 +102,40 @@ export default { timeRange: [parseTime(new Date(), '{y}-{m}-{d} 00:00:00'), parseTime(new Date(), '{y}-{m}-{d} 23:59:59')] }, loading: false, + activeGroup: '', // 当前激活的分组 + } + }, + computed: { + groupedLocationLogs() { + if (!this.locationLogList || this.locationLogList.length === 0) { + return []; + } + + const groups = []; + let currentGroup = { + status: this.locationLogList[0].status, + logs: [this.locationLogList[0]], + startTime: this.locationLogList[0].at, + endTime: this.locationLogList[0].at + }; + + for (let i = 1; i < this.locationLogList.length; i++) { + const currentLog = this.locationLogList[i]; + if (currentLog.status === currentGroup.status) { + currentGroup.logs.push(currentLog); + currentGroup.endTime = currentLog.at; + } else { + groups.push(currentGroup); + currentGroup = { + status: currentLog.status, + logs: [currentLog], + startTime: currentLog.at, + endTime: currentLog.at + }; + } + } + groups.push(currentGroup); + return groups; } }, created() { @@ -135,6 +187,22 @@ export default { this.$refs.map.handleSliderChange(index); }); } + }, + getLogIndex(groupIndex, logIndex) { + let totalIndex = 0; + for (let i = 0; i < groupIndex; i++) { + totalIndex += this.groupedLocationLogs[i].logs.length; + } + return totalIndex + logIndex; + }, + getStatusType(status) { + const statusMap = { + 0: 'info', // 离线 + 1: 'success', // 在线 + 2: 'warning', // 骑行中 + 3: 'danger' // 故障 + }; + return statusMap[status] || 'info'; } } } @@ -148,12 +216,30 @@ export default { border: 1px solid #EBEEF5; border-radius: 4px; + .group-header { + font-size: 12px; + padding-left: 8px; + + .group-time { + color: #606266; + margin-right: 8px; + } + + .group-count { + margin-left: 8px; + color: #909399; + } + } + .log-item { padding: 8px 12px; - border-bottom: 1px solid #EBEEF5; cursor: pointer; transition: all 0.3s; + &:not(:last-child) { + border-bottom: 1px solid #EBEEF5; + } + &:hover { background-color: #F5F7FA; } @@ -183,6 +269,30 @@ export default { } } + .el-collapse { + border-top: none; + border-bottom: none; + } + + .el-collapse-item__header { + padding: 8px 12px; + background-color: #F5F7FA; + border-bottom: 1px solid #EBEEF5; + font-size: 12px; + + &:hover { + background-color: #EBEEF5; + } + } + + .el-collapse-item__content { + padding: 0; + } + + .el-collapse-item__wrap { + border-bottom: none; + } + &::-webkit-scrollbar { width: 6px; } diff --git a/src/views/bst/order/util.js b/src/views/bst/order/util.js new file mode 100644 index 0000000..d034602 --- /dev/null +++ b/src/views/bst/order/util.js @@ -0,0 +1,15 @@ +import { calcSecond } from "@/utils/date"; + +export function getOrderDuration(order) { + if (order == null) { + return 0; + } + if (order.duration != null) { + return order.duration; + } + if (order.startTime != null) { + return calcSecond(order.startTime, new Date()); + } + return 0; +} + diff --git a/src/views/bst/order/view/view.vue b/src/views/bst/order/view/view.vue index 50fe9c5..05299e4 100644 --- a/src/views/bst/order/view/view.vue +++ b/src/views/bst/order/view/view.vue @@ -43,7 +43,7 @@ {{ detail.startTime | dv}} {{ detail.endTime | dv}} {{ detail.maxTime | dv}} - {{ toDescriptionFromSecond(detail.duration).text | dv}} + {{ orderDuration | dv}} {{ detail.distance / 1000 | fix2 | dv}} 公里 {{ detail.endReason | dv }} {{ detail.cancelRemark | dv }} @@ -206,6 +206,7 @@ import Operlog from '@/views/monitor/operlog/index.vue' import CommandLog from '@/views/bst/commandLog/index.vue' import DeviceLink from '@/components/Business/Device/DeviceLink.vue' import UserLink from '@/components/Business/User/UserLink.vue' +import {getOrderDuration} from '@/views/bst/order/util' export default { name: 'OrderView', @@ -238,6 +239,11 @@ export default { showVerifyDialog: false, } }, + computed: { + orderDuration() { + return toDescriptionFromSecond(getOrderDuration(this.detail)).text; + } + }, created() { this.id = this.$route.params.id this.getDetail()