设备列表
This commit is contained in:
parent
afa8e07964
commit
5d96ed97ba
|
@ -27,8 +27,6 @@
|
|||
"lib": ["dom", "esnext"],
|
||||
"types": ["@types/node"],
|
||||
"sourceMap": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"noEmit": true,
|
||||
"isolatedModules": true,
|
||||
|
|
|
@ -107,4 +107,11 @@ export function unbindDeviceArea(ids) {
|
|||
})
|
||||
}
|
||||
|
||||
|
||||
// 查询全部设备列表
|
||||
export function listAllDevice(query) {
|
||||
return request({
|
||||
url: '/bst/device/all',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
<el-card v-if="detail.id" class="card-box">
|
||||
<el-tabs>
|
||||
<el-tab-pane label="电子围栏" lazy v-if="checkPermi(['bst:areaSub:list'])">
|
||||
<area-sub :area-id="detail.id"/>
|
||||
<area-sub :area-id="detail.id" :enable-edit="false"/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="车辆列表" lazy v-if="checkPermi(['bst:device:list'])">
|
||||
<device :query="{areaId: detail.id}" />
|
||||
|
|
|
@ -90,7 +90,7 @@ export const DEVICE_STATUS_ICONS = {
|
|||
};
|
||||
|
||||
// SN标签样式
|
||||
export function getSnLabel(AMap, sn, position) {
|
||||
export function getSnLabel(AMap, sn, position, color = '#409EFF') {
|
||||
return new AMap.Text({
|
||||
text: sn,
|
||||
position: position,
|
||||
|
@ -98,10 +98,10 @@ export function getSnLabel(AMap, sn, position) {
|
|||
anchor: 'center',
|
||||
style: {
|
||||
background: '#fff',
|
||||
border: '1px solid #409EFF',
|
||||
border: `1px solid ${color}`,
|
||||
borderRadius: '4px',
|
||||
padding: '2px 8px',
|
||||
color: '#409EFF',
|
||||
color: color,
|
||||
fontSize: '12px',
|
||||
whiteSpace: 'nowrap',
|
||||
textAlign: 'center',
|
||||
|
@ -110,6 +110,25 @@ export function getSnLabel(AMap, sn, position) {
|
|||
});
|
||||
}
|
||||
|
||||
// 获取设备图标
|
||||
export function getDeviceIcon(AMap, status) {
|
||||
return new AMap.Icon({
|
||||
image: DEVICE_STATUS_ICONS[status],
|
||||
size: new AMap.Size(32, 36),
|
||||
imageSize: new AMap.Size(32, 36),
|
||||
anchor: 'bottom-center'
|
||||
});
|
||||
}
|
||||
|
||||
// 获取设备标记
|
||||
export function getDeviceMaker(AMap, icon, position) {
|
||||
return new AMap.Marker({
|
||||
position: position,
|
||||
offset: new AMap.Pixel(-16, -36),
|
||||
icon: icon
|
||||
});
|
||||
}
|
||||
|
||||
// 公共方法
|
||||
export const mapMethods = {
|
||||
// 开始绘制(不管是那种边界)
|
||||
|
|
|
@ -24,3 +24,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.device-list {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
}
|
|
@ -6,6 +6,11 @@
|
|||
<!-- 地址搜索组件 -->
|
||||
<address-search :map="map" :AMap="AMap" v-if="map && enableEdit" />
|
||||
|
||||
<!-- 车辆 -->
|
||||
<div v-if="!enableEdit && deviceList && map" class="device-list">
|
||||
<device-list :device-list="deviceList" :map="map" :AMap="AMap" ref="deviceList"/>
|
||||
</div>
|
||||
|
||||
<!-- 地图工具栏 -->
|
||||
<div class="map-tools">
|
||||
<el-button-group>
|
||||
|
@ -13,7 +18,8 @@
|
|||
<el-button size="mini" id="area-edit" type="warning" icon="el-icon-edit" @click="startAreaBoundaryEdit" v-if="enableEdit" :disabled="isEditing">电子围栏</el-button>
|
||||
<el-button size="mini" icon="el-icon-full-screen" @click="setFitView">全局查看</el-button>
|
||||
<el-button size="mini" icon="el-icon-picture" @click="toggleMapStyle">切换样式</el-button>
|
||||
<el-button size="mini" icon="el-icon-document" @click="toggleLabels">{{ showLabels ? '隐藏' : '显示' }}标签</el-button>
|
||||
<el-button size="mini" icon="el-icon-view" @click="toggleLabels">{{ showLabels ? '隐藏' : '显示' }}标签</el-button>
|
||||
<el-button size="mini" icon="el-icon-view" v-if="deviceList && deviceList.length > 0" @click="toggleDeviceShow">切换车辆展示</el-button>
|
||||
<el-button size="mini" icon="el-icon-video-play" v-if="locationLogList && locationLogList.length > 0" @click="togglePlaybackPanel">{{ isPlaybackVisible ? '隐藏' : '显示' }}轨迹控制台</el-button>
|
||||
</el-button-group>
|
||||
</div>
|
||||
|
@ -59,15 +65,16 @@ import AddressSearch from '@/views/bst/areaSub/components/AddressSearch.vue';
|
|||
import OperationArea from './mixins/OperationArea';
|
||||
import SubArea from './mixins/SubArea';
|
||||
import LocationLog from './mixins/LocationLog';
|
||||
import DeviceList from '@/views/bst/areaSub/components/DeviceList.vue'
|
||||
|
||||
export default {
|
||||
name: 'AreaMap',
|
||||
components: {
|
||||
PlaybackPanel,
|
||||
AddressSearch
|
||||
AddressSearch,
|
||||
DeviceList
|
||||
},
|
||||
mixins: [OperationArea, SubArea, LocationLog],
|
||||
dicts: ['device_status', 'device_lock_status', 'device_quality'],
|
||||
mixins: [OperationArea, SubArea, LocationLog], // 添加 DeviceList 混入
|
||||
props: {
|
||||
// 运营区域信息
|
||||
area: {
|
||||
|
@ -88,6 +95,10 @@ export default {
|
|||
enableEdit: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
deviceList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
|
@ -153,6 +164,8 @@ export default {
|
|||
contextMenu: true
|
||||
});
|
||||
|
||||
this.map.on('click', this.onCickMap);
|
||||
|
||||
// 初始化多边形编辑器
|
||||
this.polygonEditor = new this.AMap.PolygonEditor(this.map);
|
||||
|
||||
|
@ -163,6 +176,12 @@ export default {
|
|||
console.error('地图初始化失败:', error);
|
||||
}
|
||||
},
|
||||
onCickMap() {
|
||||
this.closeAreaSubInfoWindow();
|
||||
if (this.$refs.deviceList) {
|
||||
this.$refs.deviceList.closeDeviceInfoWindow();
|
||||
}
|
||||
},
|
||||
handlePolygonAdd({target}) {
|
||||
this.editingPolygon = target;
|
||||
},
|
||||
|
@ -280,6 +299,11 @@ export default {
|
|||
this.map = null;
|
||||
}
|
||||
},
|
||||
toggleDeviceShow() {
|
||||
if (this.$refs.deviceList) {
|
||||
this.$refs.deviceList.toggleDeviceShow();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
|
|
480
src/views/bst/areaSub/components/DeviceList.vue
Normal file
480
src/views/bst/areaSub/components/DeviceList.vue
Normal file
|
@ -0,0 +1,480 @@
|
|||
<template>
|
||||
<div class="device-list" :class="{ 'is-collapsed': isCollapsed }">
|
||||
<!-- 标题栏 -->
|
||||
<div class="device-list-header" @click="toggleCollapse">
|
||||
<span class="title">车辆列表</span>
|
||||
<i class="collapse-icon" :class="isCollapsed ? 'el-icon-arrow-right' : 'el-icon-arrow-left'"></i>
|
||||
</div>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<div class="device-list-content">
|
||||
<div class="filter-container">
|
||||
<el-slider
|
||||
v-model="filterForm.powerRange"
|
||||
range
|
||||
:min="0"
|
||||
:max="100"
|
||||
:marks="powerMarks"
|
||||
@change="handleFilterChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
:data="filteredDeviceList"
|
||||
style="width: 100%"
|
||||
height="400px"
|
||||
size="mini"
|
||||
@row-click="handleRowClick"
|
||||
:header-cell-style="{ background: '#f5f7fa', color: '#606266', fontWeight: '500' }"
|
||||
>
|
||||
<el-table-column prop="vehicleNum" label="车辆" width="180" align="left" show-overflow-tooltip sortable>
|
||||
<template slot-scope="scope">
|
||||
<div class="table-cell">{{ scope.row.vehicleNum || '--' }}({{ scope.row.sn }})</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" sortable>
|
||||
<template slot-scope="scope">
|
||||
<dict-tag :options="dict.type.device_status" :value="scope.row.status" size="mini"/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remainingPower" label="电量" sortable>
|
||||
<template slot-scope="scope">
|
||||
<div class="power-progress">
|
||||
<div
|
||||
class="power-bar"
|
||||
:style="{
|
||||
width: `${scope.row.remainingPower}%`,
|
||||
backgroundColor: getPowerColor(scope.row.remainingPower)
|
||||
}"
|
||||
></div>
|
||||
<span class="power-text">{{ scope.row.remainingPower | fix2 }}%</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getDeviceIcon, getDeviceMaker, getSnLabel } from "./AreaMap.js";
|
||||
|
||||
export default {
|
||||
name: 'DeviceList',
|
||||
dicts: ['device_status', 'device_lock_status', 'device_quality'],
|
||||
props: {
|
||||
deviceList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
map: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
AMap: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
infoWindow: null,
|
||||
currentDevice: null,
|
||||
deviceOverlays: new Map(),
|
||||
deviceLabelOverlays: new Map(),
|
||||
showDevice: true,
|
||||
isCollapsed: true,
|
||||
filterForm: {
|
||||
powerRange: [0, 100],
|
||||
sn: '',
|
||||
vehicleNum: ''
|
||||
},
|
||||
powerMarks: {
|
||||
0: '0%',
|
||||
20: '20%',
|
||||
40: '40%',
|
||||
60: '60%',
|
||||
80: '80%',
|
||||
100: '100%'
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filteredDeviceList() {
|
||||
return this.deviceList.filter(device => {
|
||||
const powerMatch = device.remainingPower >= this.filterForm.powerRange[0] &&
|
||||
device.remainingPower <= this.filterForm.powerRange[1];
|
||||
const snMatch = !this.filterForm.sn ||
|
||||
(device.sn && device.sn.toLowerCase().includes(this.filterForm.sn.toLowerCase()));
|
||||
const vehicleNumMatch = !this.filterForm.vehicleNum ||
|
||||
(device.vehicleNum && device.vehicleNum.toLowerCase().includes(this.filterForm.vehicleNum.toLowerCase()));
|
||||
|
||||
return powerMatch && snMatch && vehicleNumMatch;
|
||||
});
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
deviceList: {
|
||||
handler(nv) {
|
||||
this.renderDeviceMarkers(nv);
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 渲染设备标记
|
||||
renderDeviceMarkers(devices) {
|
||||
this.clearOverlays(this.deviceOverlays);
|
||||
this.clearOverlays(this.deviceLabelOverlays);
|
||||
if (!devices) return;
|
||||
|
||||
devices.forEach(device => {
|
||||
const position = [device.longitude, device.latitude];
|
||||
const icon = getDeviceIcon(this.AMap, device.status);
|
||||
const marker = getDeviceMaker(this.AMap, icon, position);
|
||||
const color = this.getPowerColor(device.remainingPower);
|
||||
const snLabel = getSnLabel(this.AMap, `${device.vehicleNum || ''} ${device.remainingPower}%`, position, color);
|
||||
|
||||
marker.on('click', () => {
|
||||
this.showDeviceInfo(device, position);
|
||||
});
|
||||
|
||||
this.deviceOverlays.set(device.id, marker);
|
||||
this.deviceLabelOverlays.set(device.id, snLabel);
|
||||
this.map.add([marker, snLabel]);
|
||||
});
|
||||
},
|
||||
|
||||
// 清除覆盖物
|
||||
clearOverlays(overlays) {
|
||||
overlays.forEach(overlay => {
|
||||
this.map.remove(overlay);
|
||||
});
|
||||
overlays.clear();
|
||||
},
|
||||
|
||||
// 获取电量颜色
|
||||
getPowerColor(power) {
|
||||
if (power <= 20) return '#F56C6C';
|
||||
if (power <= 40) return '#E6A23C';
|
||||
if (power <= 60) return '#409EFF';
|
||||
if (power <= 80) return '#67C23A';
|
||||
return '#2ecc71';
|
||||
},
|
||||
|
||||
// 获取状态类型
|
||||
getStatusType(status) {
|
||||
const statusMap = {
|
||||
'online': 'success',
|
||||
'offline': 'info',
|
||||
'fault': 'danger',
|
||||
'maintenance': 'warning'
|
||||
};
|
||||
return statusMap[status] || 'info';
|
||||
},
|
||||
|
||||
// 处理筛选条件变化
|
||||
handleFilterChange() {
|
||||
this.renderDeviceMarkers(this.filteredDeviceList);
|
||||
},
|
||||
|
||||
// 处理行点击
|
||||
handleRowClick(row) {
|
||||
const position = [row.longitude, row.latitude];
|
||||
this.map.setCenter(position);
|
||||
this.map.setZoom(15);
|
||||
this.showDeviceInfo(row, position);
|
||||
},
|
||||
|
||||
// 显示设备信息
|
||||
showDeviceInfo(device, position) {
|
||||
if (!this.infoWindow) {
|
||||
this.infoWindow = new this.AMap.InfoWindow({
|
||||
isCustom: true,
|
||||
offset: new this.AMap.Pixel(0, -30),
|
||||
autoMove: true
|
||||
});
|
||||
}
|
||||
|
||||
this.currentDevice = device;
|
||||
const content = this.createInfoWindowContent(device);
|
||||
this.infoWindow.setContent(content);
|
||||
this.infoWindow.open(this.map, position);
|
||||
},
|
||||
|
||||
// 创建信息窗口内容
|
||||
createInfoWindowContent(device) {
|
||||
const content = document.createElement('div');
|
||||
content.className = 'device-info-window';
|
||||
content.style.padding = '10px';
|
||||
content.style.minWidth = '300px';
|
||||
content.style.backgroundColor = 'rgba(255, 255, 255, 0.9)';
|
||||
content.style.borderRadius = '4px';
|
||||
content.style.boxShadow = '0 2px 12px 0 rgba(0, 0, 0, 0.1)';
|
||||
|
||||
const infoItems = [
|
||||
{ label: '车牌号', value: device.vehicleNum || '--' },
|
||||
{ label: '状态', value: this.getDictLabel('device_status', device.status) },
|
||||
{ label: 'SN', value: device.sn },
|
||||
{ label: 'MAC', value: device.mac },
|
||||
{ label: '电压', value: `${device.voltage || 0} V` },
|
||||
{ label: '电量', value: `${device.remainingPower}%` },
|
||||
{ label: '信号', value: device.signal || '-' },
|
||||
{ label: '卫星', value: device.satellites || '-' },
|
||||
{ label: '锁状态', value: this.getDictLabel('device_lock_status', device.lockStatus) },
|
||||
{ label: '电门', value: this.getDictLabel('device_quality', device.quality) },
|
||||
{ label: '速度', value: `${device.speed || 0} KM/h` },
|
||||
];
|
||||
|
||||
const gridContainer = document.createElement('div');
|
||||
gridContainer.style.display = 'grid';
|
||||
gridContainer.style.gridTemplateColumns = 'repeat(2, 1fr)';
|
||||
gridContainer.style.gap = '8px';
|
||||
gridContainer.style.fontSize = '12px';
|
||||
|
||||
infoItems.forEach(item => {
|
||||
const itemContainer = document.createElement('div');
|
||||
itemContainer.style.display = 'flex';
|
||||
itemContainer.style.alignItems = 'center';
|
||||
itemContainer.style.marginBottom = '4px';
|
||||
|
||||
const label = document.createElement('span');
|
||||
label.style.color = '#606266';
|
||||
label.style.marginRight = '4px';
|
||||
label.textContent = `${item.label}:`;
|
||||
|
||||
const value = document.createElement('span');
|
||||
value.style.color = '#303133';
|
||||
value.style.fontWeight = '500';
|
||||
value.textContent = item.value;
|
||||
|
||||
itemContainer.appendChild(label);
|
||||
itemContainer.appendChild(value);
|
||||
gridContainer.appendChild(itemContainer);
|
||||
});
|
||||
|
||||
content.appendChild(gridContainer);
|
||||
|
||||
const buttonContainer = document.createElement('div');
|
||||
buttonContainer.style.marginTop = '10px';
|
||||
buttonContainer.style.textAlign = 'center';
|
||||
|
||||
const detailButton = document.createElement('button');
|
||||
detailButton.textContent = '查看详情';
|
||||
detailButton.style.padding = '6px 12px';
|
||||
detailButton.style.backgroundColor = '#409EFF';
|
||||
detailButton.style.color = '#fff';
|
||||
detailButton.style.border = 'none';
|
||||
detailButton.style.borderRadius = '4px';
|
||||
detailButton.style.cursor = 'pointer';
|
||||
detailButton.style.fontSize = '12px';
|
||||
detailButton.onclick = () => {
|
||||
this.closeDeviceInfoWindow();
|
||||
this.$router.push(`/view/device/${device.id}`);
|
||||
};
|
||||
|
||||
buttonContainer.appendChild(detailButton);
|
||||
content.appendChild(buttonContainer);
|
||||
|
||||
return content;
|
||||
},
|
||||
|
||||
// 关闭设备信息窗口
|
||||
closeDeviceInfoWindow() {
|
||||
if (this.infoWindow) {
|
||||
this.infoWindow.close();
|
||||
this.currentDevice = null;
|
||||
}
|
||||
},
|
||||
|
||||
// 获取字典标签
|
||||
getDictLabel(dictType, value) {
|
||||
const dict = this.dict.type[dictType];
|
||||
if (!dict) return value;
|
||||
const item = dict.find(item => item.value === value);
|
||||
return item ? item.label : value;
|
||||
},
|
||||
|
||||
// 切换设备显示
|
||||
toggleDeviceShow() {
|
||||
this.showDevice = !this.showDevice;
|
||||
this.deviceOverlays.forEach(item => {
|
||||
item.setMap(this.showDevice ? this.map : null);
|
||||
});
|
||||
this.deviceLabelOverlays.forEach(item => {
|
||||
item.setMap(this.showDevice ? this.map : null);
|
||||
});
|
||||
},
|
||||
|
||||
// 切换设备列表的折叠状态
|
||||
toggleCollapse() {
|
||||
this.isCollapsed = !this.isCollapsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.device-list {
|
||||
position: relative;
|
||||
width: 400px;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
overflow: hidden;
|
||||
|
||||
&.is-collapsed {
|
||||
width: 40px;
|
||||
.device-list-content {
|
||||
display: none;
|
||||
}
|
||||
.device-list-header {
|
||||
.title {
|
||||
display: none;
|
||||
}
|
||||
.collapse-icon {
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.device-list-header {
|
||||
height: 38px;
|
||||
line-height: 38px;
|
||||
padding: 0 8px;
|
||||
background-color: #f5f7fa;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.title {
|
||||
font-size: 14px;
|
||||
color: #303133;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.collapse-icon {
|
||||
font-size: 16px;
|
||||
color: #909399;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
}
|
||||
|
||||
.device-list-content {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.filter-container {
|
||||
margin-bottom: 15px;
|
||||
|
||||
.filter-title {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.filter-form {
|
||||
.filter-item {
|
||||
width: 100%;
|
||||
margin-bottom: 15px;
|
||||
|
||||
.filter-label {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-slider {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
::v-deep .el-table {
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
|
||||
.el-table__body-wrapper {
|
||||
.el-table__row {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-cell {
|
||||
padding: 0 10px;
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
display: inline-block;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
color: #fff;
|
||||
|
||||
&.success {
|
||||
background-color: #67c23a;
|
||||
}
|
||||
|
||||
&.info {
|
||||
background-color: #909399;
|
||||
}
|
||||
|
||||
&.danger {
|
||||
background-color: #f56c6c;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
background-color: #e6a23c;
|
||||
}
|
||||
}
|
||||
|
||||
.power-progress {
|
||||
position: relative;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
background-color: #ebeef5;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin: 0 10px;
|
||||
|
||||
.power-bar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
|
||||
.power-text {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 12px;
|
||||
color: #fff;
|
||||
text-shadow: 0 0 2px rgba(0, 0, 0, 0.5);
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
// 确保单元格内容垂直居中
|
||||
.el-table__cell {
|
||||
padding: 8px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
182
src/views/bst/areaSub/components/mixins/DeviceList.js
Normal file
182
src/views/bst/areaSub/components/mixins/DeviceList.js
Normal file
|
@ -0,0 +1,182 @@
|
|||
import { getDeviceIcon, getDeviceMaker, getSnLabel } from "@/views/bst/areaSub/components/AreaMap.js";
|
||||
|
||||
export const $deviceList = {
|
||||
props: {
|
||||
deviceList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
infoWindow: null,
|
||||
currentDevice: null,
|
||||
deviceOverlays: new Map(),
|
||||
deviceLabelOverlays: new Map(),
|
||||
showDevice: true,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
deviceList(nv, ov) {
|
||||
this.renderDeviceMarkers(nv);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 新增方法,用于在地图上渲染设备标记
|
||||
renderDeviceMarkers() {
|
||||
this.clearOverlays(this.deviceOverlays);
|
||||
this.clearOverlays(this.deviceLabelOverlays);
|
||||
if (this.deviceList == null) {
|
||||
return;
|
||||
}
|
||||
this.deviceList.forEach(device => {
|
||||
const position = [device.longitude, device.latitude];
|
||||
const icon = getDeviceIcon(this.AMap, device.status);
|
||||
const marker = getDeviceMaker(this.AMap, icon, position);
|
||||
let color = '#409EFF';
|
||||
// 根据电量设置不同颜色
|
||||
if (device.remainingPower <= 20) {
|
||||
color = '#F56C6C'; // 红色 - 严重不足
|
||||
} else if (device.remainingPower <= 40) {
|
||||
color = '#E6A23C'; // 橙色 - 电量不足
|
||||
} else if (device.remainingPower <= 60) {
|
||||
color = '#409EFF'; // 蓝色 - 一般
|
||||
} else if (device.remainingPower <= 80) {
|
||||
color = '#67C23A'; // 绿色 - 充足
|
||||
} else {
|
||||
color = '#2ecc71'; // 深绿色 - 非常充足
|
||||
}
|
||||
const snLabel = getSnLabel(this.AMap, `${device.vehicleNum || ''} ${device.remainingPower}%`, position, color);
|
||||
|
||||
// 添加点击事件
|
||||
marker.on('click', (e) => {
|
||||
this.showDeviceInfo(device, position);
|
||||
});
|
||||
|
||||
this.deviceOverlays.set(device.id, marker);
|
||||
this.deviceLabelOverlays.set(device.id, snLabel);
|
||||
this.map.add([marker, snLabel]);
|
||||
});
|
||||
},
|
||||
|
||||
// 关闭信息弹窗
|
||||
closeDeviceInfoWindow() {
|
||||
if (this.infoWindow) {
|
||||
this.infoWindow.close();
|
||||
this.currentDevice = null;
|
||||
}
|
||||
},
|
||||
|
||||
// 显示设备信息弹窗
|
||||
showDeviceInfo(device, position) {
|
||||
if (!this.infoWindow) {
|
||||
this.infoWindow = new this.AMap.InfoWindow({
|
||||
isCustom: true,
|
||||
offset: new this.AMap.Pixel(0, -30),
|
||||
autoMove: true
|
||||
});
|
||||
}
|
||||
|
||||
this.currentDevice = device;
|
||||
const content = this.createInfoWindowContent(device);
|
||||
this.infoWindow.setContent(content);
|
||||
this.infoWindow.open(this.map, position);
|
||||
},
|
||||
|
||||
// 创建信息弹窗内容
|
||||
createInfoWindowContent(device) {
|
||||
const content = document.createElement('div');
|
||||
content.className = 'device-info-window';
|
||||
content.style.padding = '10px';
|
||||
content.style.minWidth = '300px';
|
||||
content.style.backgroundColor = 'rgba(255, 255, 255, 0.9)';
|
||||
content.style.borderRadius = '4px';
|
||||
content.style.boxShadow = '0 2px 12px 0 rgba(0, 0, 0, 0.1)';
|
||||
|
||||
const infoItems = [
|
||||
{ label: '车牌号', value: device.vehicleNum || '--' },
|
||||
{ label: '状态', value: this.getDictLabel('device_status', device.status) },
|
||||
{ label: 'SN', value: device.sn },
|
||||
{ label: 'MAC', value: device.mac },
|
||||
{ label: '电压', value: `${device.voltage || 0} V` },
|
||||
{ label: '电量', value: `${device.remainingPower}%` },
|
||||
{ label: '信号', value: device.signal || '-' },
|
||||
{ label: '卫星', value: device.satellites || '-' },
|
||||
{ label: '锁状态', value: this.getDictLabel('device_lock_status', device.lockStatus) },
|
||||
{ label: '电门', value: this.getDictLabel('device_quality', device.quality) },
|
||||
{ label: '速度', value: `${device.speed || 0} KM/h` },
|
||||
];
|
||||
|
||||
// 创建网格容器
|
||||
const gridContainer = document.createElement('div');
|
||||
gridContainer.style.display = 'grid';
|
||||
gridContainer.style.gridTemplateColumns = 'repeat(2, 1fr)';
|
||||
gridContainer.style.gap = '8px';
|
||||
gridContainer.style.fontSize = '12px';
|
||||
|
||||
infoItems.forEach(item => {
|
||||
const itemContainer = document.createElement('div');
|
||||
itemContainer.style.display = 'flex';
|
||||
itemContainer.style.alignItems = 'center';
|
||||
itemContainer.style.marginBottom = '4px';
|
||||
|
||||
const label = document.createElement('span');
|
||||
label.style.color = '#606266';
|
||||
label.style.marginRight = '4px';
|
||||
label.textContent = `${item.label}:`;
|
||||
|
||||
const value = document.createElement('span');
|
||||
value.style.color = '#303133';
|
||||
value.style.fontWeight = '500';
|
||||
value.textContent = item.value;
|
||||
|
||||
itemContainer.appendChild(label);
|
||||
itemContainer.appendChild(value);
|
||||
gridContainer.appendChild(itemContainer);
|
||||
});
|
||||
|
||||
content.appendChild(gridContainer);
|
||||
|
||||
// 添加查看详情按钮
|
||||
const buttonContainer = document.createElement('div');
|
||||
buttonContainer.style.marginTop = '10px';
|
||||
buttonContainer.style.textAlign = 'center';
|
||||
|
||||
const detailButton = document.createElement('button');
|
||||
detailButton.textContent = '查看详情';
|
||||
detailButton.style.padding = '6px 12px';
|
||||
detailButton.style.backgroundColor = '#409EFF';
|
||||
detailButton.style.color = '#fff';
|
||||
detailButton.style.border = 'none';
|
||||
detailButton.style.borderRadius = '4px';
|
||||
detailButton.style.cursor = 'pointer';
|
||||
detailButton.style.fontSize = '12px';
|
||||
detailButton.onclick = () => {
|
||||
this.closeDeviceInfoWindow();
|
||||
this.$router.push(`/view/device/${device.id}`);
|
||||
};
|
||||
|
||||
buttonContainer.appendChild(detailButton);
|
||||
content.appendChild(buttonContainer);
|
||||
|
||||
return content;
|
||||
},
|
||||
|
||||
// 获取字典标签
|
||||
getDictLabel(dictType, value) {
|
||||
const dict = this.dict.type[dictType];
|
||||
if (!dict) return value;
|
||||
const item = dict.find(item => item.value === value);
|
||||
return item ? item.label : value;
|
||||
},
|
||||
toggleDeviceShow() {
|
||||
this.showDevice = !this.showDevice;
|
||||
this.deviceOverlays.forEach(item => {
|
||||
item.setMap(this.showDevice ? this.map : null);
|
||||
});
|
||||
this.deviceLabelOverlays.forEach(item => {
|
||||
item.setMap(this.showDevice ? this.map : null);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import { isEmpty } from "@/utils/index";
|
||||
import { DEVICE_STATUS_ICONS, getSnLabel } from "@/views/bst/areaSub/components/AreaMap";
|
||||
import { DEVICE_STATUS_ICONS, getDeviceIcon, getDeviceMaker, getSnLabel } from "@/views/bst/areaSub/components/AreaMap";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
|
@ -126,19 +126,10 @@ export default {
|
|||
|
||||
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.currentDeviceIcon = getDeviceIcon(this.AMap, this.currentLog.status);
|
||||
|
||||
// 首次创建设备标记
|
||||
this.currentDeviceMarker = new this.AMap.Marker({
|
||||
position: position,
|
||||
offset: new this.AMap.Pixel(-16, -36),
|
||||
icon: this.currentDeviceIcon
|
||||
});
|
||||
this.currentDeviceMarker = getDeviceMaker(this.AMap, this.currentDeviceIcon, position);
|
||||
|
||||
// 保存当前设备ID
|
||||
this.currentDeviceMarker.deviceId = this.currentLog.deviceId;
|
||||
|
|
|
@ -50,16 +50,15 @@ export default {
|
|||
|
||||
methods: {
|
||||
initSubArea() {
|
||||
// 添加地图点击事件,关闭信息窗体和清除选中状态
|
||||
this.map.on('click', () => {
|
||||
this.renderSubAreas();
|
||||
},
|
||||
closeAreaSubInfoWindow() {
|
||||
if (this.currentInfoWindow) {
|
||||
this.currentInfoWindow.close();
|
||||
}
|
||||
if (!this.isEditing) {
|
||||
this.selectedAreaId = null;
|
||||
}
|
||||
});
|
||||
this.renderSubAreas();
|
||||
},
|
||||
// 渲染子区域
|
||||
renderSubAreas() {
|
||||
|
|
|
@ -38,17 +38,18 @@
|
|||
</el-form>
|
||||
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<!-- <el-col :span="1.5">
|
||||
<el-col :span="1.5" v-if="!enableEdit">
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
icon="el-icon-plus"
|
||||
icon="el-icon-location"
|
||||
size="mini"
|
||||
@click="handleAdd"
|
||||
v-has-permi="['bst:areaSub:add']"
|
||||
>新增</el-button>
|
||||
</el-col> -->
|
||||
<el-col :span="1.5">
|
||||
@click="$router.push(`/operate/areaSub/${area.id}`)"
|
||||
v-show="!enableEdit"
|
||||
v-has-permi="['bst:areaSub:list']"
|
||||
>前往编辑</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5" v-if="enableEdit">
|
||||
<el-button
|
||||
type="danger"
|
||||
plain
|
||||
|
@ -125,7 +126,7 @@
|
|||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="160">
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="160" v-if="enableEdit">
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
size="mini"
|
||||
|
@ -158,11 +159,13 @@
|
|||
ref="map"
|
||||
:area="area"
|
||||
:area-sub-list="areaSubList"
|
||||
:device-list="deviceList"
|
||||
@edit="handleUpdate"
|
||||
@delete="handleDelete"
|
||||
@add-boundary="handleAddBoundary"
|
||||
@update-boundary="handleUpdateBoundary"
|
||||
@update-area-boundary="handleUpdateAreaBoundary"
|
||||
:enableEdit="enableEdit"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -187,6 +190,7 @@ import { AreaSubStatus, AreaSubType } from '@/utils/enums';
|
|||
import AreaMap from '@/views/bst/areaSub/components/AreaMap.vue';
|
||||
import AreaSubEditDialog from '@/views/bst/areaSub/components/AreaSubEditDialog.vue';
|
||||
import {startGuide} from "@/utils/guide"
|
||||
import { listAllDevice } from '@/api/bst/device';
|
||||
|
||||
// 默认排序字段
|
||||
const defaultSort = {
|
||||
|
@ -203,6 +207,11 @@ export default {
|
|||
areaId: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
// 启用编辑功能
|
||||
enableEdit: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
|
@ -275,6 +284,7 @@ export default {
|
|||
},
|
||||
area: {},
|
||||
loadingArea: false,
|
||||
deviceList: []
|
||||
};
|
||||
},
|
||||
created() {
|
||||
|
@ -286,9 +296,11 @@ export default {
|
|||
this.getArea();
|
||||
this.getList();
|
||||
|
||||
if (this.enableEdit) {
|
||||
this.$nextTick(()=> {
|
||||
startGuide("AreaSub");
|
||||
startGuide("AreaSubEdit");
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleRowClick(row) {
|
||||
|
@ -346,10 +358,23 @@ export default {
|
|||
this.loadingArea = true;
|
||||
getArea(this.queryParams.areaId).then(response => {
|
||||
this.area = response.data;
|
||||
|
||||
if (!this.enableEdit) {
|
||||
this.getDeviceList();
|
||||
}
|
||||
}).finally(() => {
|
||||
this.loadingArea = false;
|
||||
});
|
||||
},
|
||||
// 设备列表
|
||||
getDeviceList() {
|
||||
if (this.area == null) {
|
||||
return;
|
||||
}
|
||||
listAllDevice({areaId: this.area.id}).then(res => {
|
||||
this.deviceList = res.data;
|
||||
})
|
||||
},
|
||||
/** 当排序按钮被点击时触发 **/
|
||||
onSortChange(column) {
|
||||
if (column.order == null) {
|
||||
|
|
|
@ -36,8 +36,8 @@ module.exports = {
|
|||
proxy: {
|
||||
// detail: https://cli.vuejs.org/config/#devserver-proxy
|
||||
[process.env.VUE_APP_BASE_API]: {
|
||||
target: `http://localhost:4101`,
|
||||
// target: `https://ele.ccttiot.com/prod-api`,
|
||||
// target: `http://localhost:4101`,
|
||||
target: `https://ele.ccttiot.com/prod-api`,
|
||||
changeOrigin: true,
|
||||
pathRewrite: {
|
||||
['^' + process.env.VUE_APP_BASE_API]: ''
|
||||
|
|
Loading…
Reference in New Issue
Block a user