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 @@
+
+
+
+
+
+
+
+
+
+ -
+
{{ item.name }}
+ {{ item.address }}
+
+
+
+
+
+
+
+
+
\ 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()