更新优化
This commit is contained in:
parent
905350d0e7
commit
4f254348b8
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
</el-row>
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button type="primary" @click="submitForm" :loading="submitLoading">确 定</el-button>
|
||||
<el-button @click="cancel">取 消</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
@ -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;
|
||||
})
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
212
src/views/bst/areaSub/components/AddressSearch.vue
Normal file
212
src/views/bst/areaSub/components/AddressSearch.vue
Normal file
|
@ -0,0 +1,212 @@
|
|||
<template>
|
||||
<div class="address-search">
|
||||
<el-input
|
||||
v-model="keyword"
|
||||
placeholder="请输入地址"
|
||||
size="mini"
|
||||
clearable
|
||||
@input="handleSearch"
|
||||
@clear="clearSearch"
|
||||
>
|
||||
<i slot="prefix" class="el-input__icon el-icon-search"></i>
|
||||
</el-input>
|
||||
|
||||
<!-- 搜索结果列表 -->
|
||||
<div class="search-result" v-show="searchResults.length > 0">
|
||||
<ul>
|
||||
<li
|
||||
v-for="(item, index) in searchResults"
|
||||
:key="index"
|
||||
@click="handleSelect(item)"
|
||||
class="result-item"
|
||||
>
|
||||
<div class="item-name">{{ item.name }}</div>
|
||||
<div class="item-address">{{ item.address }}</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* 地址搜索组件
|
||||
* @component AddressSearch
|
||||
* @description 用于在地图上搜索地址并定位
|
||||
*/
|
||||
export default {
|
||||
name: 'AddressSearch',
|
||||
props: {
|
||||
// 地图实例
|
||||
map: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
AMap: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
keyword: '', // 搜索关键字
|
||||
searchResults: [], // 搜索结果
|
||||
autoComplete: null, // 自动完成实例
|
||||
placeSearch: null, // 地点搜索实例
|
||||
searchTimeout: null // 防抖定时器
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.initSearchPlugins()
|
||||
},
|
||||
methods: {
|
||||
// 初始化搜索插件
|
||||
async initSearchPlugins() {
|
||||
try {
|
||||
// 创建自动完成和地点搜索实例
|
||||
this.autoComplete = new this.AMap.AutoComplete({
|
||||
type: '', // 搜索类型,默认查询所有类型
|
||||
city: '全国', // 搜索城市,默认全国
|
||||
datatype: 'all', // 返回的数据类型
|
||||
citylimit: false, // 是否强制限制在设置的城市内搜索
|
||||
input: 'keyword' // 绑定输入提示后的回调
|
||||
});
|
||||
|
||||
this.placeSearch = new this.AMap.PlaceSearch({
|
||||
map: this.map,
|
||||
pageSize: 10,
|
||||
citylimit: false,
|
||||
autoFitView: true
|
||||
});
|
||||
|
||||
// 添加错误处理
|
||||
this.autoComplete.on('error', (error) => {
|
||||
console.error('自动完成搜索错误:', error);
|
||||
this.$message.error('搜索服务出现错误,请稍后重试');
|
||||
});
|
||||
|
||||
this.placeSearch.on('error', (error) => {
|
||||
console.error('地点搜索错误:', error);
|
||||
this.$message.error('搜索服务出现错误,请稍后重试');
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('搜索插件初始化失败:', error);
|
||||
this.$message.error('搜索服务初始化失败');
|
||||
}
|
||||
},
|
||||
|
||||
// 处理搜索输入
|
||||
handleSearch() {
|
||||
if (this.searchTimeout) {
|
||||
clearTimeout(this.searchTimeout);
|
||||
}
|
||||
|
||||
// 防抖处理
|
||||
this.searchTimeout = setTimeout(() => {
|
||||
if (this.keyword.trim()) {
|
||||
// 使用 Autocomplete 进行搜索
|
||||
this.autoComplete.search(this.keyword, (status, result) => {
|
||||
|
||||
if (status === 'complete' && result.tips) {
|
||||
this.searchResults = result.tips.map(tip => ({
|
||||
name: tip.name || '',
|
||||
address: (tip.district || '') + (tip.address || ''),
|
||||
location: tip.location
|
||||
}));
|
||||
} else {
|
||||
this.searchResults = [];
|
||||
console.warn('搜索无结果或失败');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.clearSearch();
|
||||
}
|
||||
}, 300);
|
||||
},
|
||||
|
||||
// 清空搜索
|
||||
clearSearch() {
|
||||
this.searchResults = []
|
||||
this.placeSearch.clear()
|
||||
},
|
||||
|
||||
// 处理选择地址
|
||||
handleSelect(item) {
|
||||
if (item.location) {
|
||||
// 如果已经有位置信息,直接使用
|
||||
this.map.setCenter([item.location.lng, item.location.lat]);
|
||||
this.map.setZoom(15);
|
||||
this.searchResults = [];
|
||||
this.keyword = item.name;
|
||||
} else {
|
||||
// 否则使用 PlaceSearch 搜索详细信息
|
||||
this.placeSearch.search(item.name, (status, result) => {
|
||||
if (status === 'complete' && result.poiList && result.poiList.pois.length > 0) {
|
||||
const poi = result.poiList.pois[0];
|
||||
this.map.setCenter([poi.location.lng, poi.location.lat]);
|
||||
this.map.setZoom(15);
|
||||
this.searchResults = [];
|
||||
this.keyword = item.name;
|
||||
} else {
|
||||
this.$message.warning('无法获取该地址的具体位置');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.address-search {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
left: 15px;
|
||||
width: 300px;
|
||||
|
||||
.search-result {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1);
|
||||
border-radius: 4px;
|
||||
margin-top: 5px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.result-item {
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
&:hover {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
font-size: 14px;
|
||||
color: #303133;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.item-address {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -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
|
||||
];
|
||||
}
|
||||
};
|
||||
|
|
File diff suppressed because it is too large
Load Diff
347
src/views/bst/areaSub/components/mixins/LocationLog.js
Normal file
347
src/views/bst/areaSub/components/mixins/LocationLog.js
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
87
src/views/bst/areaSub/components/mixins/OperationArea.js
Normal file
87
src/views/bst/areaSub/components/mixins/OperationArea.js
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
||||
};
|
335
src/views/bst/areaSub/components/mixins/SubArea.js
Normal file
335
src/views/bst/areaSub/components/mixins/SubArea.js
Normal file
|
@ -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 = `
|
||||
<h4>${subArea.name}</h4>
|
||||
<div class="info-actions">
|
||||
<a class="info-action" data-action="edit">编辑信息</a>
|
||||
<a class="info-action" data-action="boundary">修改边界</a>
|
||||
<a class="info-action" data-action="delete">删除</a>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 直接绑定事件
|
||||
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;
|
||||
}
|
||||
}
|
||||
};
|
|
@ -17,22 +17,6 @@
|
|||
@keyup.enter.native="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="设备名称" prop="deviceName">
|
||||
<el-input
|
||||
v-model="queryParams.deviceName"
|
||||
placeholder="请输入设备名称"
|
||||
clearable
|
||||
@keyup.enter.native="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="型号" prop="modelName">
|
||||
<el-input
|
||||
v-model="queryParams.modelName"
|
||||
placeholder="请输入型号名称"
|
||||
clearable
|
||||
@keyup.enter.native="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="车牌号" prop="vehicleNum">
|
||||
<el-input
|
||||
v-model="queryParams.vehicleNum"
|
||||
|
@ -41,23 +25,6 @@
|
|||
@keyup.enter.native="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="运营区" prop="areaId">
|
||||
<area-remote-select
|
||||
v-model="queryParams.areaId"
|
||||
clearable
|
||||
@change="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="在线状态" prop="onlineStatus">
|
||||
<el-select v-model="queryParams.onlineStatus" placeholder="请选择在线状态" clearable @change="handleQuery">
|
||||
<el-option
|
||||
v-for="dict in dict.type.device_online_status"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="车辆状态" prop="status">
|
||||
<el-select v-model="queryParams.status" placeholder="请选择车辆状态" clearable @change="handleQuery">
|
||||
<el-option
|
||||
|
@ -68,16 +35,6 @@
|
|||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="锁状态" prop="lockStatus">
|
||||
<el-select v-model="queryParams.lockStatus" placeholder="请选择锁状态" clearable @change="handleQuery">
|
||||
<el-option
|
||||
v-for="dict in dict.type.device_lock_status"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="运营商" prop="mchName">
|
||||
<el-input
|
||||
v-model="queryParams.mchName"
|
||||
|
@ -86,10 +43,35 @@
|
|||
@keyup.enter.native="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="物联网状态" prop="iotStatus" label-width="6em">
|
||||
<el-select v-model="queryParams.iotStatus" placeholder="请选择物联网状态" clearable @change="handleQuery">
|
||||
<el-form-item label="运营区" prop="areaId">
|
||||
<area-remote-select
|
||||
v-model="queryParams.areaId"
|
||||
clearable
|
||||
@change="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="车型" prop="modelName">
|
||||
<el-input
|
||||
v-model="queryParams.modelName"
|
||||
placeholder="请输入车型"
|
||||
clearable
|
||||
@keyup.enter.native="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="在线状态" prop="onlineStatus">
|
||||
<el-select v-model="queryParams.onlineStatus" placeholder="请选择在线状态" clearable @change="handleQuery">
|
||||
<el-option
|
||||
v-for="dict in dict.type.device_iot_status"
|
||||
v-for="dict in dict.type.device_online_status"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="锁状态" prop="lockStatus">
|
||||
<el-select v-model="queryParams.lockStatus" placeholder="请选择锁状态" clearable @change="handleQuery">
|
||||
<el-option
|
||||
v-for="dict in dict.type.device_lock_status"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-date-picker
|
||||
v-if="queryParams.timeRange != null && queryParams.timeRange[0] != null && queryParams.timeRange[1] != null"
|
||||
v-model="queryParams.timeRange"
|
||||
value-format="yyyy-MM-dd HH:mm:ss"
|
||||
type="datetimerange"
|
||||
|
@ -24,28 +25,45 @@
|
|||
style="width: 100%;"
|
||||
/>
|
||||
<div class="location-log-list">
|
||||
<div
|
||||
@click="handleLogClick(log, index)"
|
||||
v-for="(log, index) in locationLogList"
|
||||
:key="index"
|
||||
class="log-item"
|
||||
>
|
||||
<div class="log-time">{{ log.at }}</div>
|
||||
<div class="log-info">
|
||||
<span class="info-item">
|
||||
<i class="el-icon-location"></i>
|
||||
{{ log.longitude | dv }}, {{ log.latitude | dv }}
|
||||
</span>
|
||||
<span class="info-item">
|
||||
<i class="el-icon-odometer"></i>
|
||||
{{ log.voltage }}V
|
||||
</span>
|
||||
<span class="info-item">
|
||||
{{ log.sn }}
|
||||
<dict-tag :options="dict.type.device_status" :value="log.status" size="mini"/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<el-collapse v-model="activeGroup" accordion>
|
||||
<el-collapse-item
|
||||
v-for="(group, groupIndex) in groupedLocationLogs"
|
||||
:key="groupIndex"
|
||||
:name="groupIndex"
|
||||
>
|
||||
<template slot="title">
|
||||
<div class="group-header">
|
||||
<span class="group-time">{{ group.startTime }} - {{ group.endTime }}</span>
|
||||
<dict-tag :options="dict.type.device_status" :value="group.status" size="mini"/>
|
||||
<span class="group-count">({{ group.logs.length }}条)</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="group-content">
|
||||
<div
|
||||
v-for="(log, logIndex) in group.logs"
|
||||
:key="logIndex"
|
||||
@click="handleLogClick(log, getLogIndex(groupIndex, logIndex))"
|
||||
class="log-item"
|
||||
>
|
||||
<div class="log-time">
|
||||
<dict-tag :options="dict.type.device_status" :value="log.status" size="mini"/>
|
||||
{{ log.at }}
|
||||
</div>
|
||||
<div class="log-info">
|
||||
<span class="info-item">
|
||||
<i class="el-icon-location"></i>
|
||||
{{ log.longitude.toFixed(8) }}, {{ log.latitude.toFixed(8) }}
|
||||
</span>
|
||||
<span class="info-item">
|
||||
<i class="el-icon-odometer"></i>
|
||||
{{ log.voltage | fix2 }}V
|
||||
</span>
|
||||
<span class="info-item">{{ log.sn }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
<el-empty v-if="!locationLogList || locationLogList.length === 0" description="暂无数据" />
|
||||
</div>
|
||||
</el-col>
|
||||
|
@ -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;
|
||||
}
|
||||
|
|
15
src/views/bst/order/util.js
Normal file
15
src/views/bst/order/util.js
Normal file
|
@ -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;
|
||||
}
|
||||
|
|
@ -43,7 +43,7 @@
|
|||
<el-descriptions-item label="开始时间">{{ detail.startTime | dv}}</el-descriptions-item>
|
||||
<el-descriptions-item label="结束时间">{{ detail.endTime | dv}}</el-descriptions-item>
|
||||
<el-descriptions-item label="到期时间">{{ detail.maxTime | dv}}</el-descriptions-item>
|
||||
<el-descriptions-item label="骑行时长">{{ toDescriptionFromSecond(detail.duration).text | dv}}</el-descriptions-item>
|
||||
<el-descriptions-item label="骑行时长">{{ orderDuration | dv}}</el-descriptions-item>
|
||||
<el-descriptions-item label="骑行距离">{{ detail.distance / 1000 | fix2 | dv}} 公里</el-descriptions-item>
|
||||
<el-descriptions-item label="结束原因" v-if="detail.endReason">{{ detail.endReason | dv }}</el-descriptions-item>
|
||||
<el-descriptions-item label="取消原因" v-if="detail.cancelRemark">{{ detail.cancelRemark | dv }}</el-descriptions-item>
|
||||
|
@ -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()
|
||||
|
|
Loading…
Reference in New Issue
Block a user