车辆管理界面完善
106
README.md
@ -20,17 +20,97 @@ yarn dev
|
||||
# 前端访问地址 http://localhost:80
|
||||
```
|
||||
|
||||
<template>
|
||||
<div class="common-layout">
|
||||
<el-container>
|
||||
<el-header>Header</el-header>
|
||||
<el-container>
|
||||
<el-aside width="200px">Aside</el-aside>
|
||||
<el-main>Main</el-main>
|
||||
</el-container>
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
td: 68px
|
||||
未完成任务:
|
||||
1.左侧菜单栏:刷新状态不保存
|
||||
2.左侧二级菜单选中的时候 一级菜单无背景色
|
||||
超图地图开发使用基础
|
||||
1、前期准备工作熟悉超图地图服务以及基本的操作。
|
||||
2、熟悉Openlayers以及Leaflet相关操作
|
||||
具体代码操作:
|
||||
1、定义一个地图渲染承载框<div id="replay_map" />
|
||||
2、定义一个地图实例 map: null, 定义一个渲染位置图层 layer: null
|
||||
3、初始化地图
|
||||
// 初始化地图
|
||||
initMap() {
|
||||
ol.proj.setProj4(proj4);
|
||||
proj4.defs("EPSG:4528","+proj=tmerc +lat_0=0 +lon_0=120 +k=1 +x_0=40500000 +y_0=0 +ellps=GRS80 +units=m +no_defs");
|
||||
var projection = new ol.proj.Projection({
|
||||
code: 'EPSG:4528',// 地图坐标系
|
||||
});
|
||||
this.map = new ol.Map({
|
||||
target: 'replay_map',//第一步设置的地图承载框
|
||||
controls: ol.control.defaults({
|
||||
attribution: false,
|
||||
rotate: false
|
||||
}),
|
||||
view: new ol.View({
|
||||
center: [40507885.133754 , 4025694.476392],//地图中心点
|
||||
zoom: 12,//初始时的缩放比例
|
||||
projection: projection,//坐标系等的设置
|
||||
rotation: 0.3
|
||||
}),
|
||||
//图层,这个是后期所有自定义的展示层
|
||||
layers: [
|
||||
new ol.layer.Tile({
|
||||
source: new ol.source.TileSuperMapRest({
|
||||
crossOrigin: 'anonymous',
|
||||
url: this.$map_url,
|
||||
extent: [40347872.25,2703739.74,40599933.05,5912395.20]
|
||||
}),
|
||||
projection: projection,
|
||||
})
|
||||
]
|
||||
});
|
||||
// 添加位置图层
|
||||
this.layer = new ol.layer.Vector({
|
||||
source: new ol.source.Vector(),
|
||||
zIndex: 2,
|
||||
});
|
||||
//把自定义展示的图层放到地图中
|
||||
this.map.addLayer(this.layer);
|
||||
},
|
||||
3、图层自定义内容
|
||||
//创建一个Feature
|
||||
const feature = new ol.Feature({
|
||||
//以一个点作为示例,可以是点、线、面
|
||||
geometry: new ol.geom.Point([item.longitude, item.latitude]),
|
||||
});
|
||||
//定义样式
|
||||
feature.setStyle(() => {
|
||||
//自定义样式
|
||||
const style = [];
|
||||
style.push(this.getStyle(type, item));
|
||||
return style;
|
||||
}
|
||||
//这个layer就是已开始定义的渲染图层
|
||||
this.layer.getSource().addFeature(feature);
|
||||
|
||||
1.新增弹窗的label驾驶证类型没有占一行2.人像的矩形右上角的计数器位置与编辑一致
|
||||
// 得到style实例
|
||||
getStyle(type, item, status) {
|
||||
return new ol.style.Style({
|
||||
image: new ol.style.Icon({
|
||||
src: getImage(type, item, status),
|
||||
rotateWithView: true,
|
||||
scale: this.getScale(type),
|
||||
rotation: 0,
|
||||
}),
|
||||
zIndex: 3,
|
||||
text: new ol.style.Text({
|
||||
font: '12px 微软雅黑',
|
||||
text: this.getText(type, item),
|
||||
offsetY: -15,
|
||||
fill: new ol.style.Fill({
|
||||
color: '#515a71',
|
||||
}),
|
||||
}),
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
实现功能计划:
|
||||
1.我目前需要脱离后台进行页面展示,给增加完善的模拟数据;
|
||||
2.找一个线上地址测试地图的平台概览页面显示
|
||||
3.添加新图层、绘制路线,引入绘制线、绘制面、绘制点
|
||||
@ -15,14 +15,18 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "2.0.10",
|
||||
"@supermap/iclient-ol": "^11.1.1",
|
||||
"@vueuse/core": "9.5.0",
|
||||
"axios": "0.27.2",
|
||||
"ol": "6.15.1",
|
||||
"proj4": "^2.17.0",
|
||||
"echarts": "5.4.0",
|
||||
"element-plus": "2.2.21",
|
||||
"file-saver": "2.0.5",
|
||||
"fuse.js": "6.6.2",
|
||||
"js-cookie": "3.0.1",
|
||||
"jsencrypt": "3.3.1",
|
||||
"leaflet-minimap": "^3.6.1",
|
||||
"nprogress": "0.2.0",
|
||||
"pinia": "2.0.22",
|
||||
"vue": "3.2.45",
|
||||
|
||||
BIN
src/assets/images/btn_left.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
src/assets/images/btn_right.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
src/assets/images/car.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
src/assets/images/car1.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
src/assets/images/car2.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
src/assets/images/car3.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
src/assets/images/car4.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
src/assets/images/carIcon.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
src/assets/images/carIcon1.png
Normal file
|
After Width: | Height: | Size: 442 B |
BIN
src/assets/images/carIcon2.png
Normal file
|
After Width: | Height: | Size: 463 B |
BIN
src/assets/images/carIcon3.png
Normal file
|
After Width: | Height: | Size: 365 B |
BIN
src/assets/images/carIcon4.png
Normal file
|
After Width: | Height: | Size: 370 B |
BIN
src/assets/images/carIcon5.png
Normal file
|
After Width: | Height: | Size: 370 B |
BIN
src/assets/images/fault_car.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src/assets/images/layerActive.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
src/assets/images/list_bg.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
src/assets/images/map.png
Normal file
|
After Width: | Height: | Size: 631 KiB |
BIN
src/assets/images/offline_car.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
src/assets/images/online_car.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
src/assets/images/znz.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
src/assets/images/znzActive.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
src/assets/images/zoomIn.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
src/assets/images/zoomInActive.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
src/assets/images/zoomOut.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
src/assets/images/zoomOutActive.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
@ -182,5 +182,6 @@ onMounted(() => {
|
||||
color: #999093 !important;
|
||||
padding: 0 5px !important;
|
||||
margin: 0 10px !important;
|
||||
border-radius: 4px !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
51
src/components/car/CarBreadcrumb.vue
Normal file
@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<div class="car-breadcrumb">
|
||||
<el-breadcrumb separator="/">
|
||||
<el-breadcrumb-item
|
||||
v-for="(item, index) in items"
|
||||
:key="index"
|
||||
:to="item.path"
|
||||
@click="handleClick(item, index)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineProps, defineEmits } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
items: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['click']);
|
||||
|
||||
function handleClick(item, index) {
|
||||
emit('click', { item, index });
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.car-breadcrumb {
|
||||
padding: 10px 20px;
|
||||
background-color: #292C38;
|
||||
border-bottom: 1px solid #31343E;
|
||||
}
|
||||
|
||||
:deep(.el-breadcrumb__inner) {
|
||||
color: #96A0B5;
|
||||
}
|
||||
|
||||
:deep(.el-breadcrumb__inner.is-link:hover) {
|
||||
color: #5690E7;
|
||||
}
|
||||
|
||||
:deep(.el-breadcrumb__item:last-child .el-breadcrumb__inner) {
|
||||
color: #FFFFFF;
|
||||
}
|
||||
</style>
|
||||
220
src/components/car/VehicleSearchFilter.vue
Normal file
@ -0,0 +1,220 @@
|
||||
<template>
|
||||
<div class="search-filter">
|
||||
<!-- 车牌号输入 -->
|
||||
<el-input
|
||||
v-model="searchText"
|
||||
placeholder="请输入车牌号查询"
|
||||
clearable
|
||||
class="search-input"
|
||||
@input="handleSearch"
|
||||
prefix-icon="Search"
|
||||
/>
|
||||
<!-- 车辆类型下拉 -->
|
||||
<el-select
|
||||
v-model="vehicleType"
|
||||
placeholder="车辆类型"
|
||||
clearable
|
||||
class="search-select"
|
||||
@change="handleFilter"
|
||||
>
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="驱动车" value="驱动车" />
|
||||
<el-option label="巡检车" value="巡检车" />
|
||||
<!-- 可根据实际类型补充 -->
|
||||
</el-select>
|
||||
<!-- 车辆状态下拉 -->
|
||||
<el-select
|
||||
v-model="vehicleStatus"
|
||||
placeholder="车辆状态"
|
||||
clearable
|
||||
class="search-select"
|
||||
@change="handleFilter"
|
||||
>
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="在线" value="在线" />
|
||||
<el-option label="离线" value="离线" />
|
||||
<el-option label="故障" value="故障" />
|
||||
</el-select>
|
||||
|
||||
<div class="button-group">
|
||||
<!-- 搜索按钮 -->
|
||||
<el-button type="primary" class="search-btn" @click="handleFilter">搜索</el-button>
|
||||
<!-- 重置按钮 -->
|
||||
<el-button class="reset-btn" @click="resetFilter">重置</el-button>
|
||||
</div>
|
||||
|
||||
<div class="action-group">
|
||||
<!-- 导出按钮 -->
|
||||
<el-button class="export-btn" @click="exportData">导出</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, defineEmits } from 'vue';
|
||||
import { Download, Search } from '@element-plus/icons-vue';
|
||||
|
||||
const emit = defineEmits(['search', 'filter', 'reset', 'export']);
|
||||
|
||||
const searchText = ref('');
|
||||
const vehicleType = ref('');
|
||||
const vehicleStatus = ref('');
|
||||
|
||||
// 搜索处理
|
||||
function handleSearch() {
|
||||
emit('search', {
|
||||
searchText: searchText.value,
|
||||
vehicleType: vehicleType.value,
|
||||
vehicleStatus: vehicleStatus.value
|
||||
});
|
||||
}
|
||||
|
||||
// 过滤处理
|
||||
function handleFilter() {
|
||||
emit('filter', {
|
||||
searchText: searchText.value,
|
||||
vehicleType: vehicleType.value,
|
||||
vehicleStatus: vehicleStatus.value
|
||||
});
|
||||
}
|
||||
|
||||
// 重置
|
||||
function resetFilter() {
|
||||
searchText.value = '';
|
||||
vehicleType.value = '';
|
||||
vehicleStatus.value = '';
|
||||
emit('reset');
|
||||
}
|
||||
|
||||
// 导出
|
||||
function exportData() {
|
||||
emit('export');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.search-filter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 270px;
|
||||
}
|
||||
|
||||
.search-select {
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.action-group {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.search-filter {
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
max-width: 270px;
|
||||
}
|
||||
|
||||
.action-group {
|
||||
margin-left: 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.search-filter {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.search-input, .search-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.button-group, .action-group {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep(.el-input__wrapper:hover) {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
::v-deep(.el-select) {
|
||||
border: none !important;
|
||||
width: 140px !important;
|
||||
// 修改el-select的输入框的border
|
||||
--el-border-color-hover: #343744 !important;
|
||||
--el-border-color: #343744 !important;
|
||||
}
|
||||
|
||||
::v-deep(.el-input__wrapper) {
|
||||
background: #343744 !important;
|
||||
border: none !important;
|
||||
border-radius: 8px !important;
|
||||
height: 36px !important;
|
||||
font-size: 15px;
|
||||
color: #96A0B5 !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
::v-deep(.el-select__wrapper.is-focused) {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
::v-deep(.el-select__wrapper) {
|
||||
background: #343744 !important;
|
||||
color: #96A0B5 !important;
|
||||
border-radius: 8px !important;
|
||||
height: 36px !important;
|
||||
width: 140px;
|
||||
font-size: 15px;
|
||||
border: none !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
::v-deep(.el-select__placeholder) {
|
||||
color: #96A0B5 !important;
|
||||
}
|
||||
|
||||
::v-deep(.el-button) {
|
||||
background: #343744;
|
||||
border-color: transparent;
|
||||
border-radius: 8px;
|
||||
height: 36px;
|
||||
|
||||
&.el-button--primary {
|
||||
background-color: #409eff;
|
||||
}
|
||||
|
||||
&:hover, &:focus {
|
||||
background: #2B3B5A;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
&.el-button--primary:hover, &.el-button--primary:focus {
|
||||
background-color: #337ecc;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
149
src/components/car/VehicleStats copy.vue
Normal file
@ -0,0 +1,149 @@
|
||||
<!-- 统计卡片组件 -->
|
||||
<template>
|
||||
<div class="stats-container">
|
||||
<div class="stat-cards">
|
||||
<div
|
||||
v-for="(stat, index) in stats"
|
||||
:key="index"
|
||||
class="stat-card"
|
||||
:class="{'active': modelValue === index}"
|
||||
@click="$emit('update:modelValue', index)"
|
||||
>
|
||||
<div class="stat-icon" :style="{ backgroundColor: getIconBgColor(index) }">
|
||||
<img :src="stat.icon" alt="icon" style="width: 60%; height: 60%; object-fit: contain;" />
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-title">{{ stat.title }}</div>
|
||||
<div class="stat-bottom">
|
||||
<div class="stat-value">{{ stat.value }}</div>
|
||||
<div class="stat-change">
|
||||
<span class="percent">+{{ stat.change }}%</span>
|
||||
<el-icon><ArrowUp /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ArrowUp } from '@element-plus/icons-vue';
|
||||
|
||||
defineProps({
|
||||
stats: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
modelValue: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
});
|
||||
|
||||
defineEmits(['update:modelValue']);
|
||||
|
||||
// 根据索引获取不同的图标背景色
|
||||
function getIconBgColor(index) {
|
||||
const colors = [
|
||||
'rgba(86, 108, 255, 0.15)', // 紫色背景
|
||||
'rgba(57, 196, 106, 0.15)', // 绿色背景
|
||||
'rgba(255, 168, 0, 0.15)', // 橙色背景
|
||||
'rgba(255, 86, 86, 0.15)' // 红色背景
|
||||
];
|
||||
return colors[index] || colors[0];
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.stats-container {
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
background-color: #272B38;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.stat-cards {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
flex: 1;
|
||||
background-color: #292C38;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #fff;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.stat-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stat-title {
|
||||
font-size: 14px;
|
||||
color: #96A0B5;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-bottom {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 26px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-change {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
color: #67C23A;
|
||||
}
|
||||
|
||||
.percent {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.stat-cards {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
min-width: calc(50% - 8px);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.stat-card {
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
202
src/components/car/VehicleStats.vue
Normal file
@ -0,0 +1,202 @@
|
||||
<!-- 统计卡片组件 -->
|
||||
<template>
|
||||
<div class="stats-container">
|
||||
<div class="stat-cards">
|
||||
<!-- 在线卡片 -->
|
||||
<div class="stat-card online">
|
||||
<div class="stat-icon">
|
||||
<!-- <el-icon class="icon"><CircleCheckFilled /></el-icon> -->
|
||||
<img src="../../assets/images/car1.png" alt="车辆监控">
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-title">车辆监控</div>
|
||||
<div class="stat-bottom">
|
||||
<div class="stat-value">{{ statistics.online }}</div>
|
||||
<div class="stat-change">
|
||||
<span class="percent">+30%</span>
|
||||
<el-icon><ArrowUp /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 任务中卡片 -->
|
||||
<div class="stat-card task">
|
||||
<div class="stat-icon">
|
||||
<!-- <el-icon class="icon"><Loading /></el-icon> -->
|
||||
<img src="../../assets/images/car2.png" alt="在线">
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-title">在线</div>
|
||||
<div class="stat-bottom">
|
||||
<div class="stat-value">{{ statistics.inTask }}</div>
|
||||
<div class="stat-change">
|
||||
<span class="percent">+20%</span>
|
||||
<el-icon><ArrowUp /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 故障卡片 -->
|
||||
<div class="stat-card error">
|
||||
<div class="stat-icon">
|
||||
<!-- <el-icon class="icon"><WarningFilled /></el-icon> -->
|
||||
<img src="../../assets/images/car3.png" alt="离线">
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-title">离线</div>
|
||||
<div class="stat-bottom">
|
||||
<div class="stat-value">{{ statistics.error }}</div>
|
||||
<div class="stat-change error-change">
|
||||
<span class="percent">-10%</span>
|
||||
<el-icon><ArrowDown /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 离线卡片 -->
|
||||
<div class="stat-card offline">
|
||||
<div class="stat-icon">
|
||||
<!-- <el-icon class="icon"><CloseBold /></el-icon> -->
|
||||
<img src="../../assets/images/car4.png" alt="故障">
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-title">故障</div>
|
||||
<div class="stat-bottom">
|
||||
<div class="stat-value">{{ statistics.offline }}</div>
|
||||
<div class="stat-change error-change">
|
||||
<span class="percent">-5%</span>
|
||||
<el-icon><ArrowDown /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { ArrowUp, ArrowDown, CircleCheckFilled, WarningFilled, CloseBold, Loading } from '@element-plus/icons-vue';
|
||||
|
||||
const props = defineProps({
|
||||
stats: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
// 统计数据
|
||||
const statistics = computed(() => {
|
||||
return {
|
||||
online: props.stats.find(item => item.title === '在线')?.count || 0,
|
||||
inTask: props.stats.find(item => item.title === '任务中')?.count || 0,
|
||||
error: props.stats.find(item => item.title === '故障')?.count || 0,
|
||||
offline: props.stats.find(item => item.title === '离线')?.count || 0
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.stats-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.stat-cards {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
flex: 1;
|
||||
background-color: #292c38;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #fff;
|
||||
height: 100%;
|
||||
transition: all 0.3s ease;
|
||||
min-width: 200px;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 16px;
|
||||
|
||||
.icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stat-title {
|
||||
font-size: 14px;
|
||||
color: #96a0b5;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-bottom {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.stat-change {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
color: #67c23a;
|
||||
|
||||
&.error-change {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.percent {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
// 媒体查询,处理响应式布局
|
||||
@media (max-width: 1200px) {
|
||||
.stat-cards {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
min-width: calc(50% - 10px);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.stat-cards {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
521
src/components/car/VehicleTable.vue
Normal file
@ -0,0 +1,521 @@
|
||||
<template>
|
||||
<div class="vehicle-table">
|
||||
<!-- 搜索区域 -->
|
||||
<div class="search-area">
|
||||
<el-input
|
||||
v-model="searchText"
|
||||
placeholder="请输入车牌号查询"
|
||||
clearable
|
||||
prefix-icon="Search"
|
||||
class="search-input"
|
||||
@input="handleSearchInput"
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
|
||||
<el-select v-model="vehicleType" placeholder="车辆类型" clearable class="search-select" @change="handleSearch">
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="驱动车" value="驱动车" />
|
||||
<el-option label="巡检车" value="巡检车" />
|
||||
</el-select>
|
||||
|
||||
<el-select v-model="vehicleStatus" placeholder="车辆状态" clearable class="search-select" @change="handleSearch">
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="在线" value="在线" />
|
||||
<el-option label="离线" value="离线" />
|
||||
<el-option label="故障" value="故障" />
|
||||
</el-select>
|
||||
|
||||
<div class="search-buttons">
|
||||
<el-button type="primary" class="search-btn" @click="handleSearch">搜索</el-button>
|
||||
<el-button class="reset-btn" @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
|
||||
<div class="action-buttons">
|
||||
<el-button class="export-btn" @click="handleExport">导出</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表格 -->
|
||||
<el-table
|
||||
:data="filteredVehicleData"
|
||||
style="width: 100%"
|
||||
@selection-change="handleSelectionChange"
|
||||
:header-cell-style="{ backgroundColor: '#343744', color: '#fff' }"
|
||||
class="custom-table"
|
||||
:bg-color="'#292C38'"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
|
||||
<el-table-column label="序号" width="70">
|
||||
<template #default="scope">
|
||||
{{ scope.$index + 1 }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="120">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
link
|
||||
text
|
||||
type="primary"
|
||||
|
||||
@click="handleView(scope.row)"
|
||||
>
|
||||
查看
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="carId" label="车辆号" />
|
||||
<el-table-column prop="type" label="类型" />
|
||||
<el-table-column prop="brand" label="品牌" />
|
||||
<el-table-column prop="organization" label="所属单位" width="120"/>
|
||||
<el-table-column prop="number" label="当前故障数" width="100" />
|
||||
|
||||
<el-table-column prop="routeStatus" label="作业状态">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getRouteStatusType(scope.row.routeStatus)">
|
||||
{{ scope.row.routeStatus }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="status" label="车辆状态">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === '在线' ? 'success' : 'danger'">
|
||||
{{ scope.row.status }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="chargeStatus" label="充电状态"/>
|
||||
<el-table-column prop="voltage" label="总电压"/>
|
||||
<el-table-column prop="current" label="总电流" />
|
||||
<el-table-column prop="soc" label="SOC" />
|
||||
<el-table-column prop="lastTime" label="最新时间" width="170" />
|
||||
<el-table-column prop="location" label="最新位置" />
|
||||
<el-table-column prop="batteryLevel" label="负责人" />
|
||||
<el-table-column prop="contactPhone" label="负责人电话" width="130" />
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<pagination
|
||||
v-show="total > 0"
|
||||
:total="total"
|
||||
v-model:page="currentPage"
|
||||
v-model:limit="pageSize"
|
||||
@pagination="handlePagination"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, defineProps, defineEmits, computed, watchEffect } from 'vue';
|
||||
import { Search, Download } from '@element-plus/icons-vue';
|
||||
import Pagination from '@/components/Pagination/index.vue';
|
||||
|
||||
const props = defineProps({
|
||||
vehicleData: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
filters: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['view', 'control', 'selection-change', 'search', 'filter', 'reset', 'export']);
|
||||
|
||||
// 搜索相关
|
||||
const searchText = ref('');
|
||||
const vehicleType = ref('');
|
||||
const vehicleStatus = ref('');
|
||||
|
||||
// 分页相关
|
||||
const currentPage = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const total = computed(() => props.vehicleData.length);
|
||||
|
||||
// 选中的行
|
||||
const selectedRows = ref([]);
|
||||
|
||||
// 监听filters变化并同步到本地状态
|
||||
watchEffect(() => {
|
||||
if (props.filters) {
|
||||
searchText.value = props.filters.searchText || '';
|
||||
vehicleType.value = props.filters.vehicleType || '';
|
||||
vehicleStatus.value = props.filters.vehicleStatus || '';
|
||||
}
|
||||
});
|
||||
|
||||
// 筛选后的数据
|
||||
const filteredVehicleData = computed(() => {
|
||||
let filtered = [...props.vehicleData];
|
||||
|
||||
// 应用搜索文本过滤
|
||||
if (searchText.value) {
|
||||
const text = searchText.value.toLowerCase();
|
||||
filtered = filtered.filter(item =>
|
||||
item.id?.toString().toLowerCase().includes(text) ||
|
||||
item.carId?.toLowerCase().includes(text) ||
|
||||
item.type?.toLowerCase().includes(text) ||
|
||||
item.organization?.toLowerCase().includes(text)
|
||||
);
|
||||
}
|
||||
|
||||
// 应用车辆状态过滤
|
||||
if (vehicleStatus.value) {
|
||||
filtered = filtered.filter(item => item.status === vehicleStatus.value);
|
||||
}
|
||||
|
||||
// 应用车辆类型过滤
|
||||
if (vehicleType.value) {
|
||||
filtered = filtered.filter(item => item.type === vehicleType.value);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
});
|
||||
|
||||
// 获取行驶状态对应的类型
|
||||
function getRouteStatusType(status) {
|
||||
switch (status) {
|
||||
case '任务中':
|
||||
return 'success';
|
||||
case '待命中':
|
||||
return '';
|
||||
case '充电中':
|
||||
return 'warning';
|
||||
case '等待中':
|
||||
return 'info';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
// 处理搜索输入
|
||||
function handleSearchInput() {
|
||||
// 可以添加防抖逻辑
|
||||
}
|
||||
|
||||
// 处理搜索
|
||||
function handleSearch() {
|
||||
emit('filter', {
|
||||
searchText: searchText.value,
|
||||
vehicleType: vehicleType.value,
|
||||
vehicleStatus: vehicleStatus.value
|
||||
});
|
||||
}
|
||||
|
||||
// 处理重置
|
||||
function handleReset() {
|
||||
searchText.value = '';
|
||||
vehicleType.value = '';
|
||||
vehicleStatus.value = '';
|
||||
emit('reset');
|
||||
}
|
||||
|
||||
// 处理导出
|
||||
function handleExport() {
|
||||
emit('export');
|
||||
}
|
||||
|
||||
// 处理查看详情
|
||||
function handleView(row) {
|
||||
emit('view', row);
|
||||
}
|
||||
|
||||
// 处理车辆控制
|
||||
function handleControl(row) {
|
||||
emit('control', row);
|
||||
}
|
||||
|
||||
// 处理选择变化
|
||||
function handleSelectionChange(selection) {
|
||||
selectedRows.value = selection;
|
||||
emit('selection-change', selection);
|
||||
}
|
||||
|
||||
// 处理分页变化
|
||||
function handlePagination({ page, limit }) {
|
||||
currentPage.value = page;
|
||||
pageSize.value = limit;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.vehicle-table {
|
||||
width: 100%;
|
||||
background-color: #292c38;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.search-area {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.search-input {
|
||||
width: 250px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.search-select {
|
||||
width: 150px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.search-buttons {
|
||||
margin-right: 20px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
// 输入框 下拉框 搜索栏
|
||||
:deep(.el-input__wrapper:hover) {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
:deep(.el-select) {
|
||||
border: none !important;
|
||||
width: 150px !important;
|
||||
// 修改el-select的输入框的border
|
||||
--el-border-color-hover: #343744 !important;
|
||||
--el-border-color: #343744 !important;
|
||||
}
|
||||
|
||||
:deep(.el-select__wrapper.is-focused) {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
:deep(.el-select__wrapper) {
|
||||
background: #343744 !important;
|
||||
color: #96A0B5 !important;
|
||||
border-radius: 8px !important;
|
||||
height: 36px !important;
|
||||
font-size: 15px;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
:deep(.el-input__wrapper) {
|
||||
background: #343744 !important;
|
||||
color: #96A0B5 !important;
|
||||
border-radius: 8px !important;
|
||||
height: 36px !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
:deep(.el-select__placeholder) {
|
||||
color: #96A0B5 !important;
|
||||
}
|
||||
|
||||
:deep(.el-button) {
|
||||
background: #343744;
|
||||
border-color: transparent;
|
||||
border-radius: 8px;
|
||||
height: 36px;
|
||||
|
||||
&.el-button--primary {
|
||||
background-color: #409eff;
|
||||
}
|
||||
|
||||
&:hover, &:focus {
|
||||
background: #2B3B5A;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
&.el-button--primary:hover, &.el-button--primary:focus {
|
||||
background-color: #337ecc;
|
||||
}
|
||||
}
|
||||
|
||||
/* 自定义表格样式 */
|
||||
.custom-table {
|
||||
background-color: #292c38 !important;
|
||||
color: #ffffff;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
|
||||
/* 防止表格闪白 */
|
||||
:deep(.el-loading-mask) {
|
||||
background-color: rgba(41, 44, 56, 0.7) !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__empty-block) {
|
||||
background-color: #292c38 !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__empty-text) {
|
||||
color: #96A0B5 !important;
|
||||
}
|
||||
|
||||
/* 确保整个表格区域背景色 */
|
||||
:deep(.el-table) {
|
||||
background-color: #292c38 !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__body) {
|
||||
background-color: #292c38 !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__inner-wrapper::before) {
|
||||
display: none; /* 隐藏表格顶部的边框线 */
|
||||
}
|
||||
|
||||
:deep(.el-table__header) {
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:deep(.el-table__header-wrapper) {
|
||||
th {
|
||||
background-color: #343744 !important;
|
||||
color: #96A0B5!important;
|
||||
font-weight: normal;
|
||||
border-bottom: none; /* 移除th底部边线 */
|
||||
}
|
||||
|
||||
tr th.el-table__cell:first-child {
|
||||
border-top-left-radius: 6px;
|
||||
|
||||
.cell {
|
||||
border-top-left-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
tr th.el-table__cell:last-child {
|
||||
border-top-right-radius: 4px;
|
||||
|
||||
.cell {
|
||||
border-top-right-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-table__header th.el-table__cell {
|
||||
background-color: #343744 !important;
|
||||
color: #96A0B5;
|
||||
}
|
||||
|
||||
tr {
|
||||
background-color: #343744 !important;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table__body-wrapper) {
|
||||
|
||||
td {
|
||||
height: 68px!important;
|
||||
background-color: #292c38;
|
||||
color: #ffffff;
|
||||
border-bottom: 1px solid #4C4F5F;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table__body tr:hover > td) {
|
||||
background: #2B3B5A!important;
|
||||
}
|
||||
|
||||
:deep(.el-button--link) {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #347AE2;
|
||||
padding: 0;
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
:deep(.el-button.is-text) {
|
||||
background-color: transparent !important;
|
||||
border-color: transparent;
|
||||
color: #347AE2;
|
||||
|
||||
&:hover, &:focus, &:active {
|
||||
background-color: transparent !important;
|
||||
color: #347AE2;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-button.el-button--primary.is-text) {
|
||||
background-color: transparent !important;
|
||||
color: #347AE2;
|
||||
|
||||
&:hover {
|
||||
background-color: transparent !important;
|
||||
color: #347AE2;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table__body tr:hover .el-button--link.is-text) {
|
||||
background-color: transparent !important;
|
||||
color: #347AE2;
|
||||
}
|
||||
}
|
||||
|
||||
/* 全局表格样式覆盖,防止闪白 */
|
||||
:deep(.el-table) {
|
||||
--el-table-bg-color: #292c38 !important;
|
||||
--el-table-tr-bg-color: #292c38 !important;
|
||||
--el-table-border-color: #4C4F5F !important;
|
||||
--el-table-header-bg-color: #343744 !important;
|
||||
--el-table-row-hover-bg-color: #2B3B5A !important;
|
||||
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// 媒体查询,处理响应式布局
|
||||
@media (max-width: 1200px) {
|
||||
.search-area {
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
max-width: 250px;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.search-select {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.search-buttons {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
margin-left: 0;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.search-area {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
.search-input,
|
||||
.search-select {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.search-buttons,
|
||||
.action-buttons {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
243
src/components/car/detail/BatteryOverview.vue
Normal file
@ -0,0 +1,243 @@
|
||||
<!-- BatteryOverview.vue(电池概况) -->
|
||||
<template>
|
||||
<div class="battery-overview-flex">
|
||||
<div class="left-panel">
|
||||
<div class="battery-overview-grid">
|
||||
<div class="battery-block" v-for="block in blocks" :key="block.no">
|
||||
<div class="block-title">
|
||||
<span class="block-no">{{ block.no }}</span>
|
||||
<span class="block-name">{{ block.title }}</span>
|
||||
</div>
|
||||
<ul class="block-list">
|
||||
<li v-for="item in block.items" :key="item.no">
|
||||
<span class="item-no">{{ item.no }}</span>
|
||||
<span class="item-label">{{ item.label }}</span>
|
||||
<span v-if="item.value !== undefined" class="item-value">:{{ item.value }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-panel">
|
||||
<img src="../../../assets/images/map.png" alt="地图" class="map-image">
|
||||
<!-- <div class="map-title">车辆位置</div>
|
||||
<div class="map-container">
|
||||
|
||||
<div class="map-placeholder">[地图区域]</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineProps } from 'vue';
|
||||
const props = defineProps({
|
||||
vehicle: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
// mock数据
|
||||
const batteryData = {
|
||||
voltage: 588.8,
|
||||
current: 491.4,
|
||||
soc: 89,
|
||||
chargeTotal: 289,
|
||||
dischargeTotal: 289,
|
||||
tempRange: '20~45',
|
||||
maxCellVoltage: 4.19,
|
||||
minCellVoltage: 3.98,
|
||||
bmsStatus: '高压上电状态',
|
||||
chargeStatus: '充电中',
|
||||
workStatus: '工作',
|
||||
updateTime: '2024/7/12 14:50:15',
|
||||
location: '无人车主控8001东200米',
|
||||
power: 102.3,
|
||||
mileage: 213.4,
|
||||
avgSpeed: '56.7km/h',
|
||||
socStr: '28.9kwh',
|
||||
chargeTimes: 8,
|
||||
dischargeTimes: 7,
|
||||
range: 275,
|
||||
maxPower: '299PS',
|
||||
maxTorque: '220kw',
|
||||
dcStatus: '工作',
|
||||
health: '良好',
|
||||
brand: '磷酸铁锂电池',
|
||||
};
|
||||
const blocks = [
|
||||
{
|
||||
no: '01',
|
||||
title: '电池基本参数',
|
||||
items: [
|
||||
{ no: '1.1', label: '额定电压', value: '588.8V' },
|
||||
{ no: '1.2', label: '额定电流', value: '491A' },
|
||||
{ no: '1.3', label: '标定容量', value: '289' },
|
||||
{ no: '1.4', label: '电池类型', value: '磷酸铁锂电池' },
|
||||
{ no: '1.5', label: '累计充电量', value: '289kwh' },
|
||||
{ no: '1.6', label: '累计放电量', value: '289kwh' },
|
||||
]
|
||||
},
|
||||
{
|
||||
no: '02',
|
||||
title: '状态监测',
|
||||
items: [
|
||||
{ no: '2.1', label: 'BMS状态', value: '高压上电状态' },
|
||||
{ no: '2.2', label: '车辆状态', value: '在线' },
|
||||
{ no: '2.3', label: 'DC-DC状态', value: '工作' },
|
||||
{ no: '2.4', label: '当前充电状态', value: '充电中' },
|
||||
{ no: '2.5', label: '最新更新时间', value: '2024/7/12 14:50:15' },
|
||||
{ no: '2.6', label: '最新位置', value: '无人车主控8001东200米' },
|
||||
]
|
||||
},
|
||||
{
|
||||
no: '03',
|
||||
title: '实时运行数据',
|
||||
items: [
|
||||
{ no: '3.1', label: '总电量', value: '102.3W' },
|
||||
{ no: '3.2', label: '总里程', value: '213.4A' },
|
||||
{ no: '3.3', label: 'SOC', value: '28.9kwh' },
|
||||
{ no: '3.4', label: '充电次数', value: '8' },
|
||||
{ no: '3.5', label: '放电次数', value: '7' },
|
||||
{ no: '3.6', label: '平均车速', value: '56.7km/h' },
|
||||
{ no: '3.7', label: '剩余可行驶里程', value: '275km' },
|
||||
{ no: '3.8', label: '最高充电倍率', value: '3' },
|
||||
]
|
||||
},
|
||||
{
|
||||
no: '04',
|
||||
title: '动力性能',
|
||||
items: [
|
||||
{ no: '4.1', label: '最大马力', value: '299PS' },
|
||||
{ no: '4.2', label: '最大功率', value: '220kw' },
|
||||
]
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.battery-overview-flex {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
}
|
||||
.left-panel {
|
||||
|
||||
display: flex;
|
||||
|
||||
padding:24px 32px;
|
||||
background-color: #343744;
|
||||
border-radius: 8px;
|
||||
|
||||
}
|
||||
.battery-overview-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
justify-content: space-between;
|
||||
|
||||
}
|
||||
.battery-block {
|
||||
background: #343744;
|
||||
border-radius: 8px;
|
||||
/* padding: 18px 24px; */
|
||||
width: calc(50% - 6px);
|
||||
|
||||
/* min-width: 180px; */
|
||||
margin-bottom: 0;
|
||||
color: #fff;
|
||||
box-sizing: border-box;
|
||||
|
||||
}
|
||||
.block-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.block-no {
|
||||
background: #409eff;
|
||||
color: #fff;
|
||||
border-radius: 4px;
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
padding: 2px 8px;
|
||||
margin-right: 10px;
|
||||
|
||||
}
|
||||
.block-name {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
padding-bottom: 2px;
|
||||
border-bottom: 2px solid rgba(109, 184, 255, 0.5);
|
||||
}
|
||||
.block-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.block-list li {
|
||||
font-size: 14px;
|
||||
color: #e0e0e0;
|
||||
margin-bottom: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: no-wrap;
|
||||
}
|
||||
.item-no {
|
||||
color: #BDC1C6;
|
||||
font-size: 13px;
|
||||
min-width: 2em;
|
||||
display: inline-block;
|
||||
|
||||
|
||||
}
|
||||
.item-label {
|
||||
|
||||
color: #b0b8c9;
|
||||
|
||||
}
|
||||
.item-value {
|
||||
color: #fff;
|
||||
}
|
||||
.right-panel {
|
||||
/* flex: 1; */
|
||||
/* background: #23263a; */
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.map-title {
|
||||
color: #4ea1ff;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.map-container {
|
||||
flex: 1;
|
||||
background: #1e2233;
|
||||
border-radius: 8px;
|
||||
min-height: 320px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.map-placeholder {
|
||||
color: #888;
|
||||
font-size: 18px;
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.battery-overview-flex {
|
||||
flex-direction: column;
|
||||
}
|
||||
.left-panel {
|
||||
width: 100%;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.battery-overview-grid {
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
}
|
||||
.right-panel {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
167
src/components/car/detail/ChargingStats.vue
Normal file
@ -0,0 +1,167 @@
|
||||
<!-- ChargingStats.vue(充放电统计) -->
|
||||
<template>
|
||||
<div class="charging-stats">
|
||||
<div class="top-row">
|
||||
<div class="stat-card">
|
||||
<div class="stat-title">充电总次数</div>
|
||||
<div class="stat-value">{{ stats.totalCount }}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-title">电池健康状态</div>
|
||||
<div class="stat-value">{{ stats.health }}%</div>
|
||||
</div>
|
||||
<div class="chart-area">
|
||||
<div class="chart-title">充电电时长</div>
|
||||
<div class="chart-placeholder">[充电时长柱状图]</div>
|
||||
</div>
|
||||
<div class="chart-area">
|
||||
<div class="chart-title">充放电量对比</div>
|
||||
<div class="chart-placeholder">[充放电量折线图]</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-bar">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
/>
|
||||
<el-button type="primary" @click="doFilter">筛选</el-button>
|
||||
</div>
|
||||
<el-table
|
||||
:data="pagedData"
|
||||
style="width: 100%"
|
||||
:header-cell-style="{ background: '#23263a', color: '#fff' }"
|
||||
>
|
||||
<el-table-column prop="index" label="序号" width="60" />
|
||||
<el-table-column prop="carId" label="车辆编号" />
|
||||
<el-table-column prop="vin" label="车辆识别码" />
|
||||
<el-table-column prop="chargeStatus" label="充电状态" />
|
||||
<el-table-column prop="startTime" label="开始时间" />
|
||||
<el-table-column prop="endTime" label="结束时间" />
|
||||
<el-table-column prop="chargePile" label="充电桩" />
|
||||
<el-table-column prop="operator" label="充电操作员" />
|
||||
</el-table>
|
||||
<div class="pagination-bar">
|
||||
<el-pagination
|
||||
v-model:current-page="page"
|
||||
v-model:page-size="pageSize"
|
||||
:total="filteredData.length"
|
||||
:page-sizes="[5, 10, 20]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from "vue";
|
||||
const stats = { totalCount: 50, health: 85 };
|
||||
const dateRange = ref(null);
|
||||
const page = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const allData = [
|
||||
{
|
||||
index: 1,
|
||||
carId: "DONGJIHUANUN5695",
|
||||
vin: "ANA53532156",
|
||||
chargeStatus: "停车充电",
|
||||
startTime: "2024-08-17 15:48:30",
|
||||
endTime: "2024-08-17 15:48:30",
|
||||
chargePile: "T3点位",
|
||||
operator: "张三",
|
||||
},
|
||||
{
|
||||
index: 2,
|
||||
carId: "DONGJIHUANUN5695",
|
||||
vin: "ANA53532156",
|
||||
chargeStatus: "停车充电",
|
||||
startTime: "2024-08-17 15:48:30",
|
||||
endTime: "2024-08-17 15:48:30",
|
||||
chargePile: "T3点位",
|
||||
operator: "张三",
|
||||
},
|
||||
{
|
||||
index: 3,
|
||||
carId: "DONGJIHUANUN5695",
|
||||
vin: "ANA53532156",
|
||||
chargeStatus: "停车充电",
|
||||
startTime: "2024-08-17 15:48:30",
|
||||
endTime: "2024-08-17 15:48:30",
|
||||
chargePile: "T3点位",
|
||||
operator: "张三",
|
||||
},
|
||||
];
|
||||
const filteredData = computed(() => {
|
||||
if (!dateRange.value) return allData;
|
||||
// mock筛选
|
||||
return allData;
|
||||
});
|
||||
const pagedData = computed(() => {
|
||||
const start = (page.value - 1) * pageSize.value;
|
||||
return filteredData.value.slice(start, start + pageSize.value);
|
||||
});
|
||||
function doFilter() {
|
||||
page.value = 1;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.charging-stats {
|
||||
background: #23263a;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
color: #fff;
|
||||
}
|
||||
.top-row {
|
||||
display: flex;
|
||||
gap: 18px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.stat-card {
|
||||
background: #1e2233;
|
||||
border-radius: 8px;
|
||||
padding: 18px 24px;
|
||||
min-width: 120px;
|
||||
text-align: center;
|
||||
}
|
||||
.stat-title {
|
||||
color: #4ea1ff;
|
||||
font-size: 15px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.stat-value {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.chart-area {
|
||||
background: #1e2233;
|
||||
border-radius: 8px;
|
||||
flex: 1;
|
||||
min-width: 220px;
|
||||
padding: 12px 8px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.chart-title {
|
||||
color: #4ea1ff;
|
||||
font-size: 15px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.chart-placeholder {
|
||||
color: #888;
|
||||
font-size: 16px;
|
||||
height: 120px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.filter-bar {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.pagination-bar {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
319
src/components/car/detail/FaultAlarm.vue
Normal file
@ -0,0 +1,319 @@
|
||||
<!-- FaultAlarm.vue(故障报警) -->
|
||||
<template>
|
||||
<div class="fault-alarm">
|
||||
<!-- 搜索区域 -->
|
||||
<div class="search-area">
|
||||
<el-select v-model="filter.type" placeholder="故障状态" clearable class="search-select">
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="未解除" value="未解除" />
|
||||
<el-option label="已解除" value="已解除" />
|
||||
</el-select>
|
||||
<el-select v-model="filter.status" placeholder="车辆状态" clearable class="search-select">
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="在线" value="在线" />
|
||||
<el-option label="离线" value="离线" />
|
||||
</el-select>
|
||||
|
||||
<div class="search-buttons">
|
||||
<el-button type="primary" class="search-btn" @click="doFilter">搜索</el-button>
|
||||
<el-button class="reset-btn" @click="resetFilter">重置</el-button>
|
||||
</div>
|
||||
|
||||
<div class="action-buttons">
|
||||
<el-button class="export-btn">导出</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表格 -->
|
||||
<el-table
|
||||
:data="pagedData"
|
||||
:header-cell-style="{ backgroundColor: '#343744', color: '#fff' }"
|
||||
class="custom-table">
|
||||
<el-table-column prop="index" label="序号" width="60" />
|
||||
<el-table-column prop="carId" label="电池SN码" />
|
||||
<el-table-column prop="vin" label="车辆VIN码" />
|
||||
<el-table-column prop="location" label="电池包编号" />
|
||||
<el-table-column prop="status" label="故障状态">
|
||||
<template #default="scope">
|
||||
<span :class="scope.row.status === '未解除' ? 'status-danger' : 'status-success'">
|
||||
{{ scope.row.status }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="faultType" label="故障名称" />
|
||||
<el-table-column prop="faultLevel" label="故障等级" />
|
||||
<el-table-column prop="faultStart" label="故障开始时间" />
|
||||
<el-table-column prop="faultEnd" label="故障结束时间" />
|
||||
</el-table>
|
||||
|
||||
<!-- 使用Pagination组件替换el-pagination -->
|
||||
<pagination
|
||||
v-show="filteredData.length > 0"
|
||||
:total="filteredData.length"
|
||||
v-model:page="page"
|
||||
v-model:limit="pageSize"
|
||||
@pagination="handlePagination"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from "vue";
|
||||
import Pagination from '@/components/Pagination/index.vue';
|
||||
|
||||
const filter = ref({ type: "", status: "" });
|
||||
const page = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const allData = [
|
||||
{ index: 1, carId: "DONGLIHANUN5695", vin: "AN45332156", location: "DB01; DB02", status: "未解除", faultType: "BMS内部通信故障", faultLevel: 3, faultStart: "2024-08-17 15:48:30", faultEnd: "2024-08-17 15:48:30" },
|
||||
{ index: 2, carId: "DONGLIHANUN5695", vin: "AN45332156", location: "DB01", status: "已解除", faultType: "单体过压报警", faultLevel: 1, faultStart: "2024-08-17 15:48:30", faultEnd: "2024-08-17 15:48:30" },
|
||||
{ index: 3, carId: "DONGLIHANUN5695", vin: "AN45332156", location: "DB01; DB02", status: "未解除", faultType: "BMS内部通信故障", faultLevel: 3, faultStart: "2024-08-17 15:48:30", faultEnd: "2024-08-17 15:48:30" },
|
||||
{ index: 4, carId: "DONGLIHANUN5695", vin: "AN45332156", location: "DB01", status: "已解除", faultType: "单体过压报警", faultLevel: 1, faultStart: "2024-08-17 15:48:30", faultEnd: "2024-08-17 15:48:30" },
|
||||
{ index: 5, carId: "DONGLIHANUN5695", vin: "AN45332156", location: "DB01", status: "未解除", faultType: "BMS内部通信故障", faultLevel: 3, faultStart: "2024-08-17 15:48:30", faultEnd: "2024-08-17 15:48:30" },
|
||||
{ index: 6, carId: "DONGLIHANUN5695", vin: "AN45332156", location: "DB01; DB02", status: "已解除", faultType: "单体过压报警", faultLevel: 1, faultStart: "2024-08-17 15:48:30", faultEnd: "2024-08-17 15:48:30" }
|
||||
];
|
||||
|
||||
const filteredData = computed(() => {
|
||||
return allData.filter(item => {
|
||||
const typeMatch = !filter.value.type || item.status === filter.value.type;
|
||||
const statusMatch = !filter.value.status || item.status === filter.value.status;
|
||||
return typeMatch && statusMatch;
|
||||
});
|
||||
});
|
||||
|
||||
const pagedData = computed(() => {
|
||||
const start = (page.value - 1) * pageSize.value;
|
||||
return filteredData.value.slice(start, start + pageSize.value);
|
||||
});
|
||||
|
||||
function doFilter() {
|
||||
page.value = 1;
|
||||
}
|
||||
|
||||
function resetFilter() {
|
||||
filter.value = { type: '', status: '' };
|
||||
page.value = 1;
|
||||
}
|
||||
|
||||
// 处理分页事件
|
||||
function handlePagination({ page: newPage, limit: newLimit }) {
|
||||
// 这里可以添加额外的分页处理逻辑
|
||||
// 例如重新加载数据等
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.fault-alarm {
|
||||
background: #292C38;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
// 搜索区域
|
||||
.search-area {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.search-select {
|
||||
width: 150px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.search-buttons {
|
||||
margin-right: 20px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
// 输入框和下拉框样式
|
||||
::v-deep(.el-input__wrapper:hover) {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
::v-deep(.el-select) {
|
||||
border: none !important;
|
||||
width: 160px !important;
|
||||
// 修改el-select的输入框的border
|
||||
--el-border-color-hover: #343744 !important;
|
||||
--el-border-color: #343744 !important;
|
||||
}
|
||||
|
||||
::v-deep(.el-select__wrapper.is-focused) {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
::v-deep(.el-select__wrapper) {
|
||||
background: #343744 !important;
|
||||
color: #96A0B5 !important;
|
||||
border-radius: 8px !important;
|
||||
height: 36px !important;
|
||||
width: 160px;
|
||||
font-size: 15px;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
::v-deep(.el-input__wrapper) {
|
||||
background: #343744 !important;
|
||||
color: #96A0B5 !important;
|
||||
border-radius: 8px !important;
|
||||
height: 36px !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
::v-deep(.el-select__placeholder) {
|
||||
color: #96A0B5 !important;
|
||||
}
|
||||
|
||||
// 表格样式
|
||||
.custom-table {
|
||||
background-color: #292c38 !important;
|
||||
color: #ffffff;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
|
||||
// 防止表格闪白
|
||||
:deep(.el-loading-mask) {
|
||||
background-color: rgba(41, 44, 56, 0.7) !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__empty-block) {
|
||||
background-color: #292c38 !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__empty-text) {
|
||||
color: #96A0B5 !important;
|
||||
}
|
||||
|
||||
// 确保整个表格区域背景色
|
||||
:deep(.el-table) {
|
||||
background-color: #292c38 !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__body) {
|
||||
background-color: #292c38 !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__inner-wrapper::before) {
|
||||
display: none; /* 隐藏表格顶部的边框线 */
|
||||
}
|
||||
|
||||
:deep(.el-table__header) {
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:deep(.el-table__header-wrapper) {
|
||||
th {
|
||||
background-color: #343744 !important;
|
||||
color: #96A0B5 !important;
|
||||
font-weight: normal;
|
||||
border-bottom: none; /* 移除th底部边线 */
|
||||
}
|
||||
|
||||
tr th.el-table__cell:first-child {
|
||||
border-top-left-radius: 6px;
|
||||
|
||||
.cell {
|
||||
border-top-left-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
tr th.el-table__cell:last-child {
|
||||
border-top-right-radius: 4px;
|
||||
|
||||
.cell {
|
||||
border-top-right-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-table__header th.el-table__cell {
|
||||
background-color: #343744 !important;
|
||||
color: #96A0B5;
|
||||
}
|
||||
|
||||
tr {
|
||||
background-color: #343744 !important;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table__body-wrapper) {
|
||||
td {
|
||||
height: 68px !important;
|
||||
background-color: #292c38;
|
||||
color: #ffffff;
|
||||
border-bottom: 1px solid #4C4F5F;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table__body tr:hover > td) {
|
||||
background: #2B3B5A !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 状态标识
|
||||
.status-danger {
|
||||
color: #FF4D4F;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status-success {
|
||||
color: #00C48F;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
// 移除原有的分页样式,因为已经使用了Pagination组件
|
||||
.pagination-bar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// 媒体查询,处理响应式布局
|
||||
@media (max-width: 1200px) {
|
||||
.search-area {
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
|
||||
.search-select {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.search-buttons {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
margin-left: 0;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.search-area {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
.search-select {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.search-buttons,
|
||||
.action-buttons {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
149
src/components/car/detail/TrackPlayback.vue
Normal file
@ -0,0 +1,149 @@
|
||||
<!-- TrackPlayback.vue(轨迹回放) -->
|
||||
<template>
|
||||
<div class="track-playback">
|
||||
<div class="left-list">
|
||||
<el-input
|
||||
v-model="search"
|
||||
placeholder="请输入任务号/车辆名"
|
||||
clearable
|
||||
class="search-input"
|
||||
/>
|
||||
<el-scrollbar class="task-list">
|
||||
<div
|
||||
v-for="item in filteredTasks"
|
||||
:key="item.id"
|
||||
class="task-item"
|
||||
:class="{ active: item.id === activeId }"
|
||||
@click="selectTask(item)"
|
||||
>
|
||||
<div class="task-title">{{ item.name }}</div>
|
||||
<div class="task-time">{{ item.time }}</div>
|
||||
<div class="task-status">{{ item.status }}</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
<div class="right-map">
|
||||
<div class="map-header">
|
||||
<span>{{ activeTask?.name }}</span>
|
||||
<span>{{ activeTask?.time }}</span>
|
||||
<span>{{ activeTask?.speed }}</span>
|
||||
</div>
|
||||
<div class="map-container">
|
||||
<!-- 集成Leaflet轨迹地图,暂用占位 -->
|
||||
<div class="map-placeholder">[轨迹地图区域]</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from "vue";
|
||||
const search = ref("");
|
||||
const activeId = ref(1);
|
||||
const tasks = [
|
||||
{
|
||||
id: 1,
|
||||
name: "东园区巡逻",
|
||||
time: "2024年8月16日 18:12:09",
|
||||
status: "已完成",
|
||||
speed: "28km/h",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "西园区巡逻",
|
||||
time: "2024年8月16日 18:12:09",
|
||||
status: "已完成",
|
||||
speed: "27km/h",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "南园区巡逻",
|
||||
time: "2024年8月16日 18:12:09",
|
||||
status: "已完成",
|
||||
speed: "29km/h",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "北园区巡逻",
|
||||
time: "2024年8月16日 18:12:09",
|
||||
status: "已完成",
|
||||
speed: "26km/h",
|
||||
},
|
||||
];
|
||||
const filteredTasks = computed(() => {
|
||||
if (!search.value) return tasks;
|
||||
return tasks.filter(
|
||||
(t) =>
|
||||
t.name.includes(search.value) || t.id.toString().includes(search.value)
|
||||
);
|
||||
});
|
||||
const activeTask = computed(() => tasks.find((t) => t.id === activeId.value));
|
||||
function selectTask(item) {
|
||||
activeId.value = item.id;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.track-playback {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
.left-list {
|
||||
width: 260px;
|
||||
background: #23263a;
|
||||
border-radius: 8px;
|
||||
padding: 16px 8px;
|
||||
}
|
||||
.search-input {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.task-list {
|
||||
max-height: 400px;
|
||||
}
|
||||
.task-item {
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 8px;
|
||||
cursor: pointer;
|
||||
background: #1e2233;
|
||||
color: #fff;
|
||||
}
|
||||
.task-item.active {
|
||||
background: #409eff;
|
||||
color: #fff;
|
||||
}
|
||||
.task-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
.task-time,
|
||||
.task-status {
|
||||
font-size: 13px;
|
||||
color: #b0b8c9;
|
||||
}
|
||||
.right-map {
|
||||
flex: 1;
|
||||
background: #23263a;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.map-header {
|
||||
color: #4ea1ff;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.map-container {
|
||||
flex: 1;
|
||||
background: #1e2233;
|
||||
border-radius: 8px;
|
||||
min-height: 320px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.map-placeholder {
|
||||
color: #888;
|
||||
font-size: 18px;
|
||||
}
|
||||
</style>
|
||||
95
src/components/car/detail/VideoMonitoring.vue
Normal file
@ -0,0 +1,95 @@
|
||||
<!-- VideoMonitoring.vue(视频监控) -->
|
||||
<template>
|
||||
<div class="video-monitoring">
|
||||
<div class="toolbar">
|
||||
<el-button
|
||||
:type="viewType === 'grid' ? 'primary' : 'default'"
|
||||
@click="viewType = 'grid'"
|
||||
>网格</el-button
|
||||
>
|
||||
<el-button
|
||||
:type="viewType === 'list' ? 'primary' : 'default'"
|
||||
@click="viewType = 'list'"
|
||||
>列表</el-button
|
||||
>
|
||||
</div>
|
||||
<div v-if="viewType === 'grid'" class="grid-view">
|
||||
<div v-for="item in pagedData" :key="item.id" class="video-card">
|
||||
<img :src="item.img" class="video-img" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="list-view">
|
||||
<div v-for="item in pagedData" :key="item.id" class="video-row">
|
||||
<img :src="item.img" class="video-img" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="pagination-bar">
|
||||
<el-pagination
|
||||
v-model:current-page="page"
|
||||
v-model:page-size="pageSize"
|
||||
:total="allData.length"
|
||||
:page-sizes="[6, 12, 18]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from "vue";
|
||||
const viewType = ref("grid");
|
||||
const page = ref(1);
|
||||
const pageSize = ref(6);
|
||||
const allData = Array.from({ length: 12 }).map((_, i) => ({
|
||||
id: i + 1,
|
||||
img: `https://picsum.photos/seed/${i + 1}/400/220`,
|
||||
}));
|
||||
const pagedData = computed(() => {
|
||||
const start = (page.value - 1) * pageSize.value;
|
||||
return allData.slice(start, start + pageSize.value);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.video-monitoring {
|
||||
background: #23263a;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
color: #fff;
|
||||
}
|
||||
.toolbar {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.grid-view {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 18px;
|
||||
}
|
||||
.video-card {
|
||||
background: #1e2233;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.video-img {
|
||||
width: 100%;
|
||||
height: 180px;
|
||||
object-fit: cover;
|
||||
}
|
||||
.list-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
}
|
||||
.video-row {
|
||||
background: #1e2233;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.pagination-bar {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
487
src/components/map/OpenLayersMap.vue
Normal file
@ -0,0 +1,487 @@
|
||||
<template>
|
||||
<div class="map-container">
|
||||
<div class="map-content-wrapper">
|
||||
<div ref="mapContainer" id="map"></div>
|
||||
</div>
|
||||
|
||||
<!-- 缩放控件 -->
|
||||
<OpenLayersZoomControl
|
||||
:map="map"
|
||||
:resetView="resetView"
|
||||
@compass="compass"
|
||||
@zoomIn="zoomIn"
|
||||
@zoomOut="zoomOut"
|
||||
@layerChange="handleLayerChange"
|
||||
/>
|
||||
|
||||
<!-- 地图信息 -->
|
||||
<OpenLayersMapInfo
|
||||
:map="map"
|
||||
:mapConfig="mapConfig"
|
||||
/>
|
||||
|
||||
<!-- 比例尺控件(传递map对象) -->
|
||||
<OpenLayersScaleControl :map="map" />
|
||||
|
||||
<!-- 插槽,允许父组件添加其他控件 -->
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from "vue";
|
||||
import Map from "ol/Map";
|
||||
import View from "ol/View";
|
||||
import TileLayer from "ol/layer/Tile";
|
||||
import TileGrid from "ol/tilegrid/TileGrid";
|
||||
import { TileSuperMapRest } from "@supermap/iclient-ol";
|
||||
import proj4 from "proj4";
|
||||
import { register } from "ol/proj/proj4";
|
||||
import { get as getProjection } from "ol/proj";
|
||||
import { defaults as defaultInteractions } from "ol/interaction";
|
||||
import { MouseWheelZoom } from "ol/interaction";
|
||||
import OpenLayersZoomControl from "./controls/OpenLayersZoomControl.vue";
|
||||
import OpenLayersMapInfo from "./controls/OpenLayersMapInfo.vue";
|
||||
import OverviewMap from 'ol/control/OverviewMap';
|
||||
import ScaleLine from 'ol/control/ScaleLine';
|
||||
import OpenLayersScaleControl from "./controls/OpenLayersScaleControl.vue";
|
||||
|
||||
// 注册 EPSG:4528 投影
|
||||
proj4.defs(
|
||||
"EPSG:4528",
|
||||
"+proj=tmerc +lat_0=0 +lon_0=120 +k=1 +x_0=40500000 +y_0=0 +ellps=GRS80 +units=m +no_defs"
|
||||
);
|
||||
register(proj4);
|
||||
const projection = getProjection("EPSG:4528");
|
||||
|
||||
const bounds = [
|
||||
4.05027047458807e7, // left
|
||||
4019271.0092735, // bottom
|
||||
4.0514081829214096e7, // right
|
||||
4030939.1342735, // top
|
||||
];
|
||||
const center = [4.0512494329214066e7, 4026228.757190162];
|
||||
const resolutions = [
|
||||
1322.9166666666665, 529.1666666666666, 264.5833333333333, 132.29166666666666,
|
||||
66.14583333333333, 26.45833333333333, 13.229166666666664, 6.614583333333332,
|
||||
2.645833333333333, 1.3229166666666665, 0.5291666666666666, 0.2645833333333333,
|
||||
0.13229166666666664,
|
||||
];
|
||||
|
||||
// 设置旋转角度
|
||||
const rotation = -1.26; // 逆时针旋转约72度
|
||||
|
||||
const mapContainer = ref(null);
|
||||
const map = ref(null); // 使用ref使地图实例成为响应式
|
||||
// 设置缩放级别范围
|
||||
const minZoom = 7; // 最小缩放级别
|
||||
const maxZoom = 15; // 最大缩放级别
|
||||
const initialZoom = 8; // 设置为7.5,在7和8之间
|
||||
|
||||
onMounted(() => {
|
||||
// 检查是否有保存的默认视图
|
||||
let savedView = null;
|
||||
try {
|
||||
const savedViewString = localStorage.getItem('defaultMapView');
|
||||
if (savedViewString) {
|
||||
savedView = JSON.parse(savedViewString);
|
||||
console.log('加载保存的默认视图:', savedView);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载保存的视图出错:', error);
|
||||
}
|
||||
|
||||
// 创建TileGrid
|
||||
const tileGrid = new TileGrid({
|
||||
extent: bounds,
|
||||
resolutions: resolutions,
|
||||
tileSize: 256,
|
||||
origin: [bounds[0], bounds[3]], // 重要:使用左上角为原点
|
||||
});
|
||||
|
||||
// 创建地图视图,优先使用保存的视图设置
|
||||
const view = new View({
|
||||
projection: projection,
|
||||
center: savedView ? savedView.center : center,
|
||||
zoom: savedView ? savedView.zoom : initialZoom,
|
||||
minZoom: minZoom,
|
||||
maxZoom: maxZoom,
|
||||
resolutions: resolutions,
|
||||
rotation: savedView ? savedView.rotation : rotation, // 使用保存的旋转角度或默认值
|
||||
});
|
||||
|
||||
// 创建地图,与.ol3页面完全一致
|
||||
map.value = new Map({
|
||||
target: mapContainer.value,
|
||||
controls: [], // 禁用所有默认控件,包括版权信息
|
||||
loadTilesWhileAnimating: true, // 在动画期间加载瓦片
|
||||
interactions: defaultInteractions({
|
||||
altShiftDragRotate: false, // 禁用Alt+Shift拖动旋转
|
||||
pinchRotate: false, // 禁用捏合旋转
|
||||
}).extend([
|
||||
// 自定义鼠标滚轮交互,保持旋转角度
|
||||
new MouseWheelZoom({
|
||||
constrainResolution: true,
|
||||
}),
|
||||
]),
|
||||
layers: [
|
||||
// 使用TileLayer包裹TileSuperMapRest
|
||||
new TileLayer({
|
||||
source: new TileSuperMapRest({
|
||||
url: "http://10.96.3.10:8090/iserver/services/map-QDJC_DT-GX3/rest/maps/QDJC2DZT",
|
||||
projection: "EPSG:4528",
|
||||
tileGrid: tileGrid,
|
||||
wrapX: false,
|
||||
attributions: "", // 空字符串移除属性信息
|
||||
}),
|
||||
}),
|
||||
],
|
||||
view: view,
|
||||
});
|
||||
|
||||
console.log('地图实例已创建:', map.value);
|
||||
|
||||
// 创建鹰眼图层
|
||||
const overviewLayer = new TileLayer({
|
||||
source: new TileSuperMapRest({
|
||||
url: "http://10.96.3.10:8090/iserver/services/map-QDJC_DT-GX3/rest/maps/QDJC2DZT",
|
||||
projection: "EPSG:4528",
|
||||
tileGrid: tileGrid,
|
||||
wrapX: false
|
||||
})
|
||||
});
|
||||
|
||||
// 创建鹰眼控件
|
||||
const overviewMapControl = new OverviewMap({
|
||||
className: 'ol-overviewmap custom-overview', // 用于自定义样式
|
||||
layers: [overviewLayer],
|
||||
collapsed: false,
|
||||
collapsible: false,
|
||||
rotateWithView: true,
|
||||
tipLabel: '', // 移除提示文本
|
||||
// 新增view属性,设置鹰眼内部地图的缩放级别
|
||||
view: new View({
|
||||
projection: projection,
|
||||
center: center,
|
||||
zoom: 10, // 你可以根据需要调整这个值,数字越大越放大
|
||||
minZoom: 3,
|
||||
maxZoom: 15,
|
||||
resolutions: resolutions
|
||||
})
|
||||
});
|
||||
|
||||
// 添加控件到地图
|
||||
map.value.addControl(overviewMapControl);
|
||||
|
||||
// 确保鹰眼图正确显示
|
||||
setTimeout(() => {
|
||||
const overviewElement = document.querySelector('.custom-overview');
|
||||
if (overviewElement) {
|
||||
overviewElement.style.display = 'block';
|
||||
console.log('鹰眼图已添加到地图');
|
||||
} else {
|
||||
console.warn('未找到鹰眼图元素');
|
||||
}
|
||||
}, 500);
|
||||
|
||||
|
||||
|
||||
// 确保地图完全加载后再次调整视图
|
||||
setTimeout(() => {
|
||||
// 使用fit方法让地图内容居中显示
|
||||
map.value.getView().fit(bounds, {
|
||||
size: map.value.getSize(),
|
||||
padding: [50, 50, 50, 50], // 添加内边距,确保内容不贴边
|
||||
maxZoom: 15,
|
||||
});
|
||||
|
||||
// 应用旋转,确保rotation设置有效
|
||||
map.value.getView().animate({
|
||||
rotation: rotation,
|
||||
duration: 500,
|
||||
});
|
||||
|
||||
// 让地图重新计算尺寸,确保正确显示
|
||||
map.value.updateSize();
|
||||
}, 200);
|
||||
|
||||
// 自适应拖拽方向
|
||||
// 监听地图旋转变化,自动调整拖拽方向
|
||||
view.on("change:rotation", function () {
|
||||
// 当地图旋转时,不需要额外操作,OpenLayers内部会自动处理拖拽方向
|
||||
// 但我们可以在这里做一些自定义处理,比如更新UI等
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (map.value) {
|
||||
map.value.setTarget(null);
|
||||
}
|
||||
});
|
||||
|
||||
// 供子组件调用的地图操作方法
|
||||
function zoomIn() {
|
||||
if (map.value) {
|
||||
map.value.getView().setZoom(map.value.getView().getZoom() + 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
function zoomOut() {
|
||||
if (map.value) {
|
||||
map.value.getView().setZoom(map.value.getView().getZoom() - 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
function compass() {
|
||||
if (map.value) {
|
||||
// 重置旋转和中心
|
||||
map.value.getView().setRotation(rotation);
|
||||
map.value.getView().setCenter(center);
|
||||
map.value.getView().setZoom(initialZoom);
|
||||
}
|
||||
}
|
||||
|
||||
// 可选:重置视图方法,供控件调用
|
||||
function resetView() {
|
||||
if (map.value) {
|
||||
// 检查是否有保存的默认视图
|
||||
try {
|
||||
const savedViewString = localStorage.getItem('defaultMapView');
|
||||
if (savedViewString) {
|
||||
const savedView = JSON.parse(savedViewString);
|
||||
map.value.getView().setCenter(savedView.center);
|
||||
map.value.getView().setZoom(savedView.zoom);
|
||||
map.value.getView().setRotation(savedView.rotation);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载保存的视图出错:', error);
|
||||
}
|
||||
|
||||
// 如果没有保存的视图,使用默认值
|
||||
map.value.getView().setRotation(rotation);
|
||||
map.value.getView().setCenter(center);
|
||||
map.value.getView().setZoom(initialZoom);
|
||||
}
|
||||
}
|
||||
|
||||
// 地图配置信息,传递给MapInfo组件
|
||||
const mapConfig = {
|
||||
center,
|
||||
rotation,
|
||||
initialZoom,
|
||||
bounds: {
|
||||
left: bounds[0],
|
||||
bottom: bounds[1],
|
||||
right: bounds[2],
|
||||
top: bounds[3]
|
||||
}
|
||||
};
|
||||
|
||||
// 图层切换处理函数
|
||||
function handleLayerChange(layerType) {
|
||||
console.log('切换图层类型:', layerType);
|
||||
// 这里可以实现图层切换逻辑
|
||||
}
|
||||
|
||||
// 添加一个方法来获取当前地图状态
|
||||
function getCurrentMapState() {
|
||||
if (map.value) {
|
||||
const view = map.value.getView();
|
||||
const currentCenter = view.getCenter();
|
||||
const currentZoom = view.getZoom();
|
||||
const currentRotation = view.getRotation();
|
||||
|
||||
console.log('当前地图状态:');
|
||||
console.log('中心点:', currentCenter);
|
||||
console.log('缩放级别:', currentZoom);
|
||||
console.log('旋转角度:', currentRotation);
|
||||
|
||||
return {
|
||||
center: currentCenter,
|
||||
zoom: currentZoom,
|
||||
rotation: currentRotation
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 保存当前视图为默认视图
|
||||
function saveCurrentView() {
|
||||
const currentState = getCurrentMapState();
|
||||
if (currentState) {
|
||||
// 这里可以将当前状态保存到localStorage或后端
|
||||
localStorage.setItem('defaultMapView', JSON.stringify(currentState));
|
||||
alert('当前视图已保存为默认视图');
|
||||
|
||||
// 更新mapConfig以便其他组件使用
|
||||
mapConfig.center = currentState.center;
|
||||
mapConfig.initialZoom = currentState.zoom;
|
||||
mapConfig.rotation = currentState.rotation;
|
||||
}
|
||||
}
|
||||
|
||||
// 打印当前视图状态
|
||||
function printCurrentView() {
|
||||
const currentState = getCurrentMapState();
|
||||
if (currentState) {
|
||||
console.log('%c当前视图状态 - 复制这些值到代码中:', 'background: #4CAF50; color: white; padding: 4px; border-radius: 4px;');
|
||||
console.log(`const center = [${currentState.center[0]}, ${currentState.center[1]}];`);
|
||||
console.log(`const initialZoom = ${currentState.zoom};`);
|
||||
console.log(`const rotation = ${currentState.rotation};`);
|
||||
|
||||
alert(`当前视图状态已打印到控制台。\n\n中心点: [${currentState.center[0].toFixed(2)}, ${currentState.center[1].toFixed(2)}]\n缩放级别: ${currentState.zoom.toFixed(2)}\n旋转角度: ${currentState.rotation.toFixed(2)}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 暴露方法给父组件使用
|
||||
defineExpose({
|
||||
zoomIn,
|
||||
zoomOut,
|
||||
compass,
|
||||
resetView,
|
||||
getCurrentMapState // 暴露获取当前地图状态的方法
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.map-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex; /* 使用flex布局 */
|
||||
flex-direction: column;
|
||||
background-color: #eeece1;
|
||||
}
|
||||
|
||||
.map-content-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
flex: 1; /* 填充剩余空间 */
|
||||
background-color: #eeece1;
|
||||
}
|
||||
|
||||
#map {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #eeece1;
|
||||
z-index: 1;
|
||||
overflow: visible !important; /* 允许内容溢出 */
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
/* 确保OpenLayers内部元素正确显示 */
|
||||
:deep(.ol-viewport) {
|
||||
background-color: #eeece1 !important;
|
||||
}
|
||||
|
||||
/* 隐藏所有OpenLayers控件,但保留小地图 */
|
||||
:deep(.ol-control:not(.custom-overview)) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* 隐藏OpenLayers属性信息 */
|
||||
:deep(.ol-attribution) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
:deep(.custom-overview) {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
left: 20px;
|
||||
background-color: rgba(255, 255, 255, 0.7);
|
||||
border-radius: 4px;
|
||||
padding: 2px;
|
||||
border: 1px solid #ccc;
|
||||
display: block !important; /* 强制显示 */
|
||||
z-index: 1000; /* 确保在其他元素之上 */
|
||||
}
|
||||
:deep(.custom-overview .ol-overviewmap-map) {
|
||||
width: 160px !important;
|
||||
height: 160px !important;
|
||||
}
|
||||
:deep(.custom-overview button) {
|
||||
display: none;
|
||||
}
|
||||
:deep(.custom-overview .ol-overviewmap-box) {
|
||||
border: 2px solid #1869BD;
|
||||
}
|
||||
|
||||
:deep(.custom-scale-line) {
|
||||
position: absolute;
|
||||
left: 30px;
|
||||
bottom: 190px;
|
||||
background: rgba(255,255,255,0.8);
|
||||
border-radius: 2px;
|
||||
padding: 2px 8px;
|
||||
font-size: 12px;
|
||||
color: #27313F;
|
||||
z-index: 1200;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
|
||||
.save-view-btn {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
z-index: 3000;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.save-view-btn:hover {
|
||||
background-color: #fff;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 768px) {
|
||||
.save-view-btn {
|
||||
padding: 6px 10px;
|
||||
font-size: 12px;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.print-view-btn {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 140px;
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
z-index: 3000;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.print-view-btn:hover {
|
||||
background-color: #fff;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 768px) {
|
||||
.print-view-btn {
|
||||
padding: 6px 10px;
|
||||
font-size: 12px;
|
||||
top: 10px;
|
||||
right: 120px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
120
src/components/map/controls/OpenLayersMapInfo.vue
Normal file
@ -0,0 +1,120 @@
|
||||
<template>
|
||||
<div class="map-info">
|
||||
<input type="text" readonly :value="xyText" />
|
||||
<input type="text" readonly :value="wgs84Text" />
|
||||
<!-- <input type="text" readonly :value="pixelText" /> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue';
|
||||
import { transform } from 'ol/proj';
|
||||
|
||||
const props = defineProps({
|
||||
map: Object
|
||||
});
|
||||
|
||||
const xyText = ref('XY坐标: --');
|
||||
const wgs84Text = ref('WGS84: --');
|
||||
// const pixelText = ref('像素: --');
|
||||
|
||||
function handleMouseMove(event) {
|
||||
if (!props.map) {
|
||||
console.warn('地图实例未传递到OpenLayersMapInfo组件');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. 获取像素坐标
|
||||
const pixel = [event.offsetX, event.offsetY];
|
||||
// console.log('鼠标像素坐标:', pixel);
|
||||
|
||||
// 2. 获取投影坐标
|
||||
const coordinate = props.map.getCoordinateFromPixel(pixel);
|
||||
// console.log('投影坐标:', coordinate);
|
||||
|
||||
if (coordinate) {
|
||||
xyText.value = `XY坐标: X=${coordinate[0].toFixed(6)}, Y=${coordinate[1].toFixed(6)}`;
|
||||
|
||||
// 3. 转换为WGS84
|
||||
try {
|
||||
const [lng, lat] = transform(coordinate, 'EPSG:4528', 'EPSG:4326');
|
||||
console.log('WGS84坐标:', lng, lat);
|
||||
wgs84Text.value = `WGS84: 经度=${lng.toFixed(6)}°, 纬度=${lat.toFixed(6)}°`;
|
||||
} catch (error) {
|
||||
console.error('坐标转换失败:', error);
|
||||
wgs84Text.value = 'WGS84: 转换错误';
|
||||
}
|
||||
} else {
|
||||
xyText.value = 'XY坐标: --';
|
||||
wgs84Text.value = 'WGS84: --';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('处理鼠标移动事件时出错:', error);
|
||||
xyText.value = 'XY坐标: 错误';
|
||||
wgs84Text.value = 'WGS84: 错误';
|
||||
}
|
||||
}
|
||||
|
||||
function setupListener() {
|
||||
if (!props.map) return;
|
||||
const mapElement = props.map.getTargetElement();
|
||||
if (mapElement) {
|
||||
mapElement.addEventListener('mousemove', handleMouseMove);
|
||||
}
|
||||
}
|
||||
function cleanupListener() {
|
||||
if (!props.map) return;
|
||||
const mapElement = props.map.getTargetElement();
|
||||
if (mapElement) {
|
||||
mapElement.removeEventListener('mousemove', handleMouseMove);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(setupListener);
|
||||
onUnmounted(cleanupListener);
|
||||
watch(() => props.map, (newMap, oldMap) => {
|
||||
cleanupListener();
|
||||
setupListener();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.map-info {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
right: 25px;
|
||||
z-index: 3000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
pointer-events: none;
|
||||
padding: 5px;
|
||||
border-radius: 4px;
|
||||
background-color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
.map-info input {
|
||||
padding: 4px;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
color: #333;
|
||||
width: 350px;
|
||||
margin-bottom: 2px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 768px) {
|
||||
.map-info {
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
.map-info input {
|
||||
width: 250px;
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
23
src/components/map/controls/OpenLayersMiniMap.vue
Normal file
@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<!-- 缩略图容器 -->
|
||||
<div class="mini-map-container" ref="miniMapContainer"></div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.mini-map-container {
|
||||
position: absolute;
|
||||
bottom: 50px;
|
||||
left: 30px;
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
z-index: 9999;
|
||||
border: 2px solid rgba(0,0,0,0.3);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.4);
|
||||
background-color: white;
|
||||
transition: all 0.3s ease, opacity 0.5s ease-in;
|
||||
opacity: 1; /* 修改为可见 */
|
||||
will-change: opacity, transform; /* 优化渲染性能 */
|
||||
}
|
||||
</style>
|
||||
170
src/components/map/controls/OpenLayersScaleControl.vue
Normal file
@ -0,0 +1,170 @@
|
||||
<template>
|
||||
<div class="scale-control">
|
||||
<div class="scale-bar">
|
||||
<div class="scale-line" :style="{ width: scaleWidth + 'px' }"></div>
|
||||
<div class="scale-ticks" :style="{ width: scaleWidth + 'px' }">
|
||||
<div class="tick left"></div>
|
||||
<div class="tick right"></div>
|
||||
</div>
|
||||
<div class="scale-text">{{ scaleText }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, onMounted, onUnmounted } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
map: Object,
|
||||
position: {
|
||||
type: String,
|
||||
default: 'bottomleft' // 默认位置在左下角
|
||||
}
|
||||
});
|
||||
|
||||
// 响应式状态
|
||||
const scaleText = ref('100 米');
|
||||
const scaleWidth = ref(80);
|
||||
|
||||
// 更新比例尺
|
||||
function updateScale() {
|
||||
if (!props.map) return;
|
||||
|
||||
const view = props.map.getView();
|
||||
if (!view) return;
|
||||
|
||||
const resolution = view.getResolution();
|
||||
if (!resolution) return;
|
||||
|
||||
// 设定比例尺线段的像素宽度范围
|
||||
const minWidth = 60; // 最小宽度
|
||||
const maxWidth = 150; // 最大宽度
|
||||
|
||||
// 计算实际距离(米)
|
||||
let meters = resolution * 100; // 基于100像素计算距离
|
||||
|
||||
// 让比例尺线段宽度更贴合整数距离
|
||||
// 取常用的比例尺长度(如1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000...)
|
||||
const niceScales = [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 50000, 100000];
|
||||
|
||||
// 找到最接近的"漂亮"比例尺值
|
||||
let best = niceScales[0];
|
||||
for (let i = 0; i < niceScales.length; i++) {
|
||||
if (meters < niceScales[i]) {
|
||||
// 如果当前值已经超过了meters,我们选择前一个或当前值
|
||||
best = i === 0 ? niceScales[0] :
|
||||
(meters - niceScales[i-1] < niceScales[i] - meters) ?
|
||||
niceScales[i-1] : niceScales[i];
|
||||
break;
|
||||
}
|
||||
if (i === niceScales.length - 1) {
|
||||
best = niceScales[i]; // 如果到达数组末尾,使用最大值
|
||||
}
|
||||
}
|
||||
|
||||
// 重新计算线段宽度(像素)
|
||||
let width = best / resolution;
|
||||
|
||||
// 限制宽度在合理范围内
|
||||
width = Math.max(minWidth, Math.min(maxWidth, width));
|
||||
scaleWidth.value = Math.round(width);
|
||||
|
||||
// 设置显示文本
|
||||
if (best >= 1000) {
|
||||
scaleText.value = (best / 1000) + ' 公里';
|
||||
} else {
|
||||
scaleText.value = best + ' 米';
|
||||
}
|
||||
}
|
||||
|
||||
// 监听地图事件
|
||||
function setupListeners() {
|
||||
if (!props.map) return;
|
||||
|
||||
// 使用moveend事件,它会在任何导致分辨率变化的操作后触发
|
||||
// 包括缩放、平移等
|
||||
props.map.on('moveend', updateScale);
|
||||
|
||||
// 初始更新一次
|
||||
updateScale();
|
||||
}
|
||||
|
||||
// 清理事件监听
|
||||
function cleanupListeners() {
|
||||
if (props.map) {
|
||||
props.map.un('moveend', updateScale);
|
||||
}
|
||||
}
|
||||
|
||||
// 监听map属性变化
|
||||
watch(() => props.map, (newMap) => {
|
||||
cleanupListeners(); // 清理旧的监听器
|
||||
if (newMap) {
|
||||
setupListeners();
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
setupListeners();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
cleanupListeners();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.scale-control {
|
||||
position: absolute;
|
||||
left: 30px;
|
||||
bottom: 190px;
|
||||
z-index: 1200;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.scale-bar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background-color: rgba(255, 255, 255, 0.7);
|
||||
padding: 3px 6px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.scale-line {
|
||||
height: 3px;
|
||||
background-color: #27313F;
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
.scale-ticks {
|
||||
position: relative;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.tick {
|
||||
position: absolute;
|
||||
width: 2px;
|
||||
height: 8px;
|
||||
background-color: #27313F;
|
||||
bottom: -3px;
|
||||
}
|
||||
|
||||
.tick.left {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.tick.right {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.scale-text {
|
||||
font-size: 12px;
|
||||
color: #27313F;
|
||||
font-weight: normal;
|
||||
margin-top: 4px;
|
||||
white-space: nowrap;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
</style>
|
||||
131
src/components/map/controls/OpenLayersZoomControl.vue
Normal file
@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<div class="zoom-control">
|
||||
<LayerSwitcher class="layer-switcher" :map="map" @layerChange="onLayerChange" ref="layerSwitcherRef" />
|
||||
|
||||
<div class="compass" @click="onCompass"></div>
|
||||
<div class="zoom-in" @click="onZoomIn"></div>
|
||||
<div class="zoom-out" @click="onZoomOut"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
// import LayerSwitcher from './LayerSwitcher.vue';
|
||||
|
||||
// 定义props接受地图实例
|
||||
const props = defineProps({
|
||||
map: Object, // 地图实例
|
||||
resetView: {
|
||||
type: Function,
|
||||
default: null
|
||||
}
|
||||
});
|
||||
|
||||
// 组件引用
|
||||
const layerSwitcherRef = ref(null);
|
||||
|
||||
// 向父组件发出事件
|
||||
const emit = defineEmits(['compass', 'zoomIn', 'zoomOut', 'layerChange']);
|
||||
|
||||
// 指南针/重置视图
|
||||
function onCompass() {
|
||||
emit('compass');
|
||||
if (props.resetView) {
|
||||
props.resetView();
|
||||
} else if (props.map) {
|
||||
// 如果没有提供重置函数,尝试使用地图的初始视图
|
||||
const initialCenter = props.map.options.center;
|
||||
const initialZoom = props.map.options.zoom || 7;
|
||||
props.map.setView(initialCenter, initialZoom, { animate: true });
|
||||
}
|
||||
}
|
||||
|
||||
// 放大地图
|
||||
function onZoomIn() {
|
||||
emit('zoomIn');
|
||||
}
|
||||
|
||||
// 缩小地图
|
||||
function onZoomOut() {
|
||||
emit('zoomOut');
|
||||
}
|
||||
|
||||
// 图层变化处理
|
||||
function onLayerChange(layerInfo) {
|
||||
console.log('ZoomControl收到图层变化:', layerInfo);
|
||||
// 将图层信息传递给父组件
|
||||
emit('layerChange', layerInfo);
|
||||
}
|
||||
|
||||
// 导出方法供外部调用
|
||||
defineExpose({
|
||||
// 设置图层可见性的方法
|
||||
setLayerVisibility(layerName, visible) {
|
||||
if (layerSwitcherRef.value) {
|
||||
return layerSwitcherRef.value.setLayerVisibility(layerName, visible);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.zoom-control {
|
||||
position: absolute;
|
||||
bottom: 250px;
|
||||
left: 20px;
|
||||
z-index: 3000;
|
||||
width: 40px;
|
||||
height: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
gap: 25px;
|
||||
}
|
||||
|
||||
.compass {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: url("../../../assets/images/znz.png") no-repeat;
|
||||
background-size: 100% 100%;
|
||||
cursor: pointer;
|
||||
z-index: 3000;
|
||||
}
|
||||
.layer-switcher{
|
||||
z-index: 3000;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: url("../../../assets/images/layer.png") no-repeat;
|
||||
background-size: 100% 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.zoom-in {
|
||||
background: url("../../../assets/images/zoomOut.png") no-repeat;
|
||||
background-size: 100% 100%;
|
||||
z-index: 3000;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.zoom-out {
|
||||
background: url("../../../assets/images/zoomIn.png") no-repeat;
|
||||
background-size: 100% 100%;
|
||||
z-index: 3000;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 768px) {
|
||||
.zoom-in, .zoom-out, .compass {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background-size: 80%;
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
164
src/components/map/info/carClarm.vue
Normal file
@ -0,0 +1,164 @@
|
||||
<template>
|
||||
<div class="car-alarm-container">
|
||||
<div class="car-item">
|
||||
<img src="../../../assets/images/online_car.png" class="car-icon" />
|
||||
<div class="car-text">
|
||||
<div class="car-count">
|
||||
{{ onlineCount }}<span class="unit">辆</span>
|
||||
</div>
|
||||
<div class="car-label">在线车辆</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="car-item">
|
||||
<img src="../../../assets/images/offline_car.png" class="car-icon" />
|
||||
<div class="car-text">
|
||||
<div class="car-count">
|
||||
{{ offlineCount }}<span class="unit">辆</span>
|
||||
</div>
|
||||
<div class="car-label">离线车辆</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="car-item">
|
||||
<img src="../../../assets/images/fault_car.png" class="car-icon" />
|
||||
<div class="car-text">
|
||||
<div class="car-count">
|
||||
{{ faultCount }}<span class="unit">辆</span>
|
||||
</div>
|
||||
<div class="car-label">故障车辆</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from "vue";
|
||||
|
||||
// 定义props,可以从外部传入数据
|
||||
const props = defineProps({
|
||||
onlineCountProp: {
|
||||
type: Number,
|
||||
default: 206,
|
||||
},
|
||||
offlineCountProp: {
|
||||
type: Number,
|
||||
default: 25,
|
||||
},
|
||||
faultCountProp: {
|
||||
type: Number,
|
||||
default: 27,
|
||||
},
|
||||
});
|
||||
|
||||
// 响应式数据,用于显示车辆数量
|
||||
const onlineCount = ref(props.onlineCountProp);
|
||||
const offlineCount = ref(props.offlineCountProp);
|
||||
const faultCount = ref(props.faultCountProp);
|
||||
|
||||
// 暴露方法,允许父组件更新数据
|
||||
defineExpose({
|
||||
updateCounts,
|
||||
});
|
||||
|
||||
// 更新车辆数量的方法
|
||||
function updateCounts(online, offline, fault) {
|
||||
onlineCount.value = online !== undefined ? online : onlineCount.value;
|
||||
offlineCount.value = offline !== undefined ? offline : offlineCount.value;
|
||||
faultCount.value = fault !== undefined ? fault : faultCount.value;
|
||||
}
|
||||
|
||||
// 组件挂载时的逻辑
|
||||
onMounted(() => {
|
||||
console.log("车辆报警信息组件已加载");
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.car-alarm-container {
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
top: 90px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap:24px;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.car-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
gap:8px;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.car-icon {
|
||||
width: 60px;
|
||||
height: auto;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
|
||||
.car-text {
|
||||
display: flex;
|
||||
text-align: left;
|
||||
flex-direction: column;
|
||||
gap:5px 0;
|
||||
color:#4E718F;
|
||||
}
|
||||
|
||||
.car-count {
|
||||
display: inline-flex;
|
||||
align-items: flex-end;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.unit {
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
margin-left: 2px;
|
||||
line-height: 1;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
.online .car-count {
|
||||
color: #4794ff;
|
||||
}
|
||||
|
||||
|
||||
.car-label {
|
||||
font-size: 14px;
|
||||
color: #4E718F;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.split-line {
|
||||
width: 1px;
|
||||
height: 30px;
|
||||
background-color: #ddd;
|
||||
margin: 0 15px;
|
||||
}
|
||||
|
||||
/* 响应式适配 */
|
||||
@media (max-width: 768px) {
|
||||
.car-alarm-container {
|
||||
flex-direction: column;
|
||||
padding: 10px;
|
||||
left: 10px;
|
||||
top: 120px;
|
||||
}
|
||||
|
||||
.car-item {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.split-line {
|
||||
width: 80%;
|
||||
height: 1px;
|
||||
margin: 8px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
479
src/components/map/info/eventlist.vue
Normal file
@ -0,0 +1,479 @@
|
||||
<template>
|
||||
<div class="event-list-container">
|
||||
<div class="event-list-header">
|
||||
<div class="header-title">车辆列表 ({{ totalCount }})</div>
|
||||
<div class="header-actions">
|
||||
<div class="filter-dropdown">
|
||||
<span class="selected-filter">全部</span>
|
||||
<span class="arrow-down"></span>
|
||||
<div class="dropdown-menu">
|
||||
<div class="menu-item">全部</div>
|
||||
<div class="menu-item">在线</div>
|
||||
<div class="menu-item">离线</div>
|
||||
<div class="menu-item">故障</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-dropdown">
|
||||
<span class="selected-filter">全部</span>
|
||||
<span class="arrow-down"></span>
|
||||
<div class="dropdown-menu">
|
||||
<div class="menu-item">驱鸟车</div>
|
||||
<div class="menu-item">牵引车</div>
|
||||
<div class="menu-item">接驳车</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="search-box">
|
||||
<input type="text" placeholder="请输入关键字搜索" />
|
||||
<div class="search-icon"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="event-list-content">
|
||||
<div
|
||||
v-for="(car, index) in carList"
|
||||
:key="car.id"
|
||||
class="car-item"
|
||||
:class="{ 'fault': car.status === 'fault', 'online': car.status === 'online', 'offline': car.status === 'offline' }"
|
||||
>
|
||||
<div class="car-main-info">
|
||||
<div class="car-id">{{ car.id }} ({{ car.type }})</div>
|
||||
<div class="car-actions">
|
||||
<div class="status-btn" :class="car.taskStatus">{{ getTaskStatusText(car.taskStatus) }}</div>
|
||||
<div class="online-status" :class="car.status">{{ getStatusText(car.status) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="car-details">
|
||||
<div class="detail-row">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">行驶速度:</span>
|
||||
<span class="detail-value">{{ car.speed }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">行驶距离:</span>
|
||||
<span class="detail-value">{{ car.distance }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">任务进度:</span>
|
||||
<span class="detail-value">{{ car.progress }}%</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">负责人:</span>
|
||||
<span class="detail-value">{{ car.manager }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="car-owner">
|
||||
<span class="owner-icon"></span>
|
||||
<span class="owner-name">{{ car.owner }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="event-list-pagination">
|
||||
<div class="page-btn active">1</div>
|
||||
<div class="page-btn">2</div>
|
||||
<div class="page-btn">3</div>
|
||||
<div class="page-btn">4</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
|
||||
// 车辆数据
|
||||
const carList = ref([
|
||||
{
|
||||
id: 'QN001',
|
||||
type: '作业车',
|
||||
status: 'online',
|
||||
taskStatus: 'task',
|
||||
speed: '56.7km/h',
|
||||
distance: '56.7km/h',
|
||||
progress: 60,
|
||||
manager: '张三',
|
||||
owner: '天人主控001'
|
||||
},
|
||||
{
|
||||
id: 'QN002',
|
||||
type: '巡查车',
|
||||
status: 'offline',
|
||||
taskStatus: 'idle',
|
||||
speed: '56.7km/h',
|
||||
distance: '56.7km/h',
|
||||
progress: 60,
|
||||
manager: '张三',
|
||||
owner: '天人主控001'
|
||||
},
|
||||
{
|
||||
id: 'QN003',
|
||||
type: '巡查车',
|
||||
status: 'fault',
|
||||
taskStatus: 'repair',
|
||||
speed: '56.7km/h',
|
||||
distance: '56.7km/h',
|
||||
progress: 60,
|
||||
manager: '张三',
|
||||
owner: '天人主控001'
|
||||
},
|
||||
{
|
||||
id: 'QN004',
|
||||
type: '巡查车',
|
||||
status: 'online',
|
||||
taskStatus: 'task',
|
||||
speed: '56.7km/h',
|
||||
distance: '56.7km/h',
|
||||
progress: 60,
|
||||
manager: '张三',
|
||||
owner: '天人主控001'
|
||||
},
|
||||
{
|
||||
id: 'QN005',
|
||||
type: '巡查车',
|
||||
status: 'fault',
|
||||
taskStatus: 'repair',
|
||||
speed: '56.7km/h',
|
||||
distance: '56.7km/h',
|
||||
progress: 60,
|
||||
manager: '张三',
|
||||
owner: '天人主控001'
|
||||
}
|
||||
]);
|
||||
|
||||
// 计算总数
|
||||
const totalCount = computed(() => carList.value.length);
|
||||
|
||||
// 获取状态文本
|
||||
function getStatusText(status) {
|
||||
switch(status) {
|
||||
case 'online': return '在线';
|
||||
case 'offline': return '离线';
|
||||
case 'fault': return '故障';
|
||||
default: return '未知';
|
||||
}
|
||||
}
|
||||
|
||||
// 获取任务状态文本
|
||||
function getTaskStatusText(taskStatus) {
|
||||
switch(taskStatus) {
|
||||
case 'task': return '任务中';
|
||||
case 'idle': return '闲置中';
|
||||
case 'repair': return '维修中';
|
||||
default: return '未知';
|
||||
}
|
||||
}
|
||||
|
||||
// 暴露方法,允许父组件更新数据
|
||||
defineExpose({
|
||||
updateCarList,
|
||||
});
|
||||
|
||||
// 更新车辆列表的方法
|
||||
function updateCarList(newList) {
|
||||
if (newList && Array.isArray(newList)) {
|
||||
carList.value = newList;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
console.log('车辆列表组件已加载');
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.event-list-container {
|
||||
background: url("../../../assets/images/list_bg.png") no-repeat;
|
||||
background-size: 100% 100%;
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 391px;
|
||||
height: 80vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* background-color: rgba(24, 33, 45, 0.85); */
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
z-index: 1000;
|
||||
color: #fff;
|
||||
/* box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); */
|
||||
}
|
||||
|
||||
|
||||
.header-title {
|
||||
font-size: 16px;
|
||||
line-height: 1.8;
|
||||
font-weight: bold;
|
||||
margin-left: 35px;
|
||||
padding:0 10px;
|
||||
color: #fff;
|
||||
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding:0 16px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.filter-dropdown {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: rgba(19, 26, 36, 0.5);
|
||||
border-radius: 4px;
|
||||
padding: 5px 10px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.selected-filter {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.arrow-down {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 5px solid transparent;
|
||||
border-right: 5px solid transparent;
|
||||
border-top: 5px solid #fff;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
background: #253243;
|
||||
border-radius: 4px;
|
||||
width: 100%;
|
||||
z-index: 10;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.filter-dropdown:hover .dropdown-menu {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
padding: 8px 10px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.menu-item:hover {
|
||||
background: rgba(78, 113, 143, 0.2);
|
||||
}
|
||||
|
||||
.search-box {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
background: rgba(19, 26, 36, 0.5);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 6px 30px 6px 10px;
|
||||
color: #fff;
|
||||
width: 150px;
|
||||
font-size: 13px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.search-box input::placeholder {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23ffffff'%3E%3Cpath d='M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
.event-list-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding:0px 16px 16px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
|
||||
.car-item {
|
||||
padding: 10px;
|
||||
background-color: #424851;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.car-main-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.car-id {
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.car-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.status-btn {
|
||||
padding: 3px 6px;
|
||||
border-radius: 2px;
|
||||
font-size: 11px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-btn.task {
|
||||
background-color: #00B1EB;
|
||||
}
|
||||
|
||||
.status-btn.idle {
|
||||
background-color: #BDBDBD;
|
||||
}
|
||||
|
||||
.status-btn.repair {
|
||||
background-color: #E8D254;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.car-details {
|
||||
font-size: 13px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.car-owner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.owner-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin-right: 5px;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23ffffff'%3E%3Cpath d='M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: contain;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.online-status {
|
||||
padding: 3px 6px;
|
||||
border-radius: 2px;
|
||||
font-size: 11px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.online-status.online {
|
||||
background-color: #00B1EB;
|
||||
}
|
||||
|
||||
.online-status.offline {
|
||||
background-color: #BDBDBD;
|
||||
}
|
||||
|
||||
.online-status.fault {
|
||||
background-color: #B51318;
|
||||
}
|
||||
|
||||
.event-list-pagination {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 10px 16px;
|
||||
gap: 5px;
|
||||
/* background-color: rgba(19, 26, 36, 0.5); */
|
||||
}
|
||||
|
||||
.page-btn {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
background-color: rgba(36, 76, 120, 0.25);
|
||||
}
|
||||
|
||||
.page-btn.active {
|
||||
background-color: rgba(36, 76, 120, 0.5);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.page-btn:hover:not(.active) {
|
||||
background-color: rgba(36, 76, 120, 0.5);
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
.event-list-content::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
}
|
||||
|
||||
.event-list-content::-webkit-scrollbar-track {
|
||||
background: rgba(19, 26, 36, 0.5);
|
||||
}
|
||||
|
||||
.event-list-content::-webkit-scrollbar-thumb {
|
||||
background: rgba(78, 113, 143, 0.5);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.event-list-content::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(78, 113, 143, 0.8);
|
||||
}
|
||||
|
||||
/* 响应式适配 */
|
||||
@media (max-width: 768px) {
|
||||
.event-list-container {
|
||||
width: 280px;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
309
src/composables/useVehicle.js
Normal file
@ -0,0 +1,309 @@
|
||||
import { ref, computed } from 'vue';
|
||||
import car1 from '../assets/images/car1.png';
|
||||
import car2 from '../assets/images/car2.png';
|
||||
import car3 from '../assets/images/car3.png';
|
||||
import car4 from '../assets/images/car4.png';
|
||||
|
||||
export function useVehicle() {
|
||||
// 车辆数据
|
||||
const vehicleData = ref([
|
||||
{
|
||||
id: 1,
|
||||
carId: 'QN001',
|
||||
type: '驱动车',
|
||||
brand: '特斯拉',
|
||||
organization: '物流管理公司',
|
||||
routeStatus: '任务中',
|
||||
status: '在线',
|
||||
chargeStatus: '停车充电',
|
||||
voltage: '360V',
|
||||
current: '200A',
|
||||
soc: '90%',
|
||||
lastTime: '2024-08-17 15:48:30',
|
||||
batteryLevel: 'T3高位',
|
||||
location: '张三',
|
||||
contactPhone: '12345678920',
|
||||
battery: 85,
|
||||
runningStatus: '任务中'
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
carId: 'QN001',
|
||||
type: '驱动车',
|
||||
brand: '特斯拉',
|
||||
organization: '物流管理公司',
|
||||
routeStatus: '任务中',
|
||||
status: '在线',
|
||||
chargeStatus: '停车充电',
|
||||
voltage: '360V',
|
||||
current: '200A',
|
||||
soc: '90%',
|
||||
lastTime: '2024-08-17 15:48:30',
|
||||
batteryLevel: 'T3高位',
|
||||
location: '张三',
|
||||
contactPhone: '12345678920',
|
||||
battery: 85,
|
||||
runningStatus: '任务中'
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
carId: 'QN001',
|
||||
type: '驱动车',
|
||||
brand: '特斯拉',
|
||||
organization: '物流管理公司',
|
||||
routeStatus: '任务中',
|
||||
status: '在线',
|
||||
chargeStatus: '停车充电',
|
||||
voltage: '360V',
|
||||
current: '200A',
|
||||
soc: '90%',
|
||||
lastTime: '2024-08-17 15:48:30',
|
||||
batteryLevel: 'T3高位',
|
||||
location: '张三',
|
||||
contactPhone: '12345678920',
|
||||
battery: 85,
|
||||
runningStatus: '任务中'
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
carId: 'QN001',
|
||||
type: '驱动车',
|
||||
brand: '特斯拉',
|
||||
organization: '物流管理公司',
|
||||
routeStatus: '任务中',
|
||||
status: '在线',
|
||||
chargeStatus: '停车充电',
|
||||
voltage: '360V',
|
||||
current: '200A',
|
||||
soc: '90%',
|
||||
lastTime: '2024-08-17 15:48:30',
|
||||
batteryLevel: 'T3高位',
|
||||
location: '张三',
|
||||
contactPhone: '12345678920',
|
||||
battery: 85,
|
||||
runningStatus: '任务中'
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
carId: 'QN001',
|
||||
type: '驱动车',
|
||||
brand: '特斯拉',
|
||||
organization: '物流管理公司',
|
||||
routeStatus: '任务中',
|
||||
status: '在线',
|
||||
chargeStatus: '停车充电',
|
||||
voltage: '360V',
|
||||
current: '200A',
|
||||
soc: '90%',
|
||||
lastTime: '2024-08-17 15:48:30',
|
||||
batteryLevel: 'T3高位',
|
||||
location: '张三',
|
||||
contactPhone: '12345678920',
|
||||
battery: 85,
|
||||
runningStatus: '任务中'
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
carId: 'QN001',
|
||||
type: '驱动车',
|
||||
brand: '特斯拉',
|
||||
organization: '物流管理公司',
|
||||
routeStatus: '任务中',
|
||||
status: '在线',
|
||||
chargeStatus: '停车充电',
|
||||
voltage: '360V',
|
||||
current: '200A',
|
||||
soc: '90%',
|
||||
lastTime: '2024-08-17 15:48:30',
|
||||
batteryLevel: 'T3高位',
|
||||
location: '张三',
|
||||
contactPhone: '12345678920',
|
||||
battery: 85,
|
||||
runningStatus: '任务中'
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
carId: 'QN001',
|
||||
type: '驱动车',
|
||||
brand: '特斯拉',
|
||||
organization: '物流管理公司',
|
||||
routeStatus: '任务中',
|
||||
status: '在线',
|
||||
chargeStatus: '停车充电',
|
||||
voltage: '360V',
|
||||
current: '200A',
|
||||
soc: '90%',
|
||||
lastTime: '2024-08-17 15:48:30',
|
||||
batteryLevel: 'T3高位',
|
||||
location: '张三',
|
||||
contactPhone: '12345678920',
|
||||
battery: 85,
|
||||
runningStatus: '任务中'
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
carId: 'QN001',
|
||||
type: '驱动车',
|
||||
brand: '特斯拉',
|
||||
organization: '物流管理公司',
|
||||
routeStatus: '任务中',
|
||||
status: '在线',
|
||||
chargeStatus: '停车充电',
|
||||
voltage: '360V',
|
||||
current: '200A',
|
||||
soc: '90%',
|
||||
lastTime: '2024-08-17 15:48:30',
|
||||
batteryLevel: 'T3高位',
|
||||
location: '张三',
|
||||
contactPhone: '12345678920',
|
||||
battery: 85,
|
||||
runningStatus: '任务中'
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
carId: 'QN001',
|
||||
type: '驱动车',
|
||||
brand: '特斯拉',
|
||||
organization: '物流管理公司',
|
||||
routeStatus: '任务中',
|
||||
status: '在线',
|
||||
chargeStatus: '停车充电',
|
||||
voltage: '360V',
|
||||
current: '200A',
|
||||
soc: '90%',
|
||||
lastTime: '2024-08-17 15:48:30',
|
||||
batteryLevel: 'T3高位',
|
||||
location: '张三',
|
||||
contactPhone: '12345678920',
|
||||
battery: 85,
|
||||
runningStatus: '任务中'
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
carId: 'QN001',
|
||||
type: '驱动车',
|
||||
brand: '特斯拉',
|
||||
organization: '物流管理公司',
|
||||
routeStatus: '任务中',
|
||||
status: '在线',
|
||||
chargeStatus: '停车充电',
|
||||
voltage: '360V',
|
||||
current: '200A',
|
||||
soc: '90%',
|
||||
lastTime: '2024-08-17 15:48:30',
|
||||
batteryLevel: 'T3高位',
|
||||
location: '张三',
|
||||
contactPhone: '12345678920',
|
||||
battery: 85,
|
||||
runningStatus: '任务中'
|
||||
}
|
||||
|
||||
]);
|
||||
|
||||
// 过滤条件
|
||||
const filters = ref({
|
||||
searchText: '',
|
||||
vehicleType: '',
|
||||
vehicleStatus: ''
|
||||
});
|
||||
|
||||
// 统计数据
|
||||
const vehicleStats = computed(() => [
|
||||
{
|
||||
title: '车辆总数',
|
||||
value: vehicleData.value.length.toString().padStart(5, '0'),
|
||||
icon: car1,
|
||||
change: 30
|
||||
},
|
||||
{
|
||||
title: '在线',
|
||||
value: vehicleData.value.filter(v => v.status === '在线').length.toString().padStart(5, '0'),
|
||||
icon: car2,
|
||||
change: 30
|
||||
},
|
||||
{
|
||||
title: '离线',
|
||||
value: vehicleData.value.filter(v => v.status === '离线').length.toString().padStart(5, '0'),
|
||||
icon: car3,
|
||||
change: 30
|
||||
},
|
||||
{
|
||||
title: '故障',
|
||||
value: vehicleData.value.filter(v => v.status === '故障').length.toString().padStart(5, '0'),
|
||||
icon: car4,
|
||||
change: 30
|
||||
}
|
||||
]);
|
||||
|
||||
// 筛选后的数据
|
||||
const filteredVehicleData = computed(() => {
|
||||
let filtered = [...vehicleData.value];
|
||||
|
||||
if (filters.value.searchText) {
|
||||
const searchText = filters.value.searchText.toLowerCase();
|
||||
filtered = filtered.filter(item =>
|
||||
item.id?.toString().toLowerCase().includes(searchText) ||
|
||||
item.carId?.toLowerCase().includes(searchText) ||
|
||||
item.type?.toLowerCase().includes(searchText) ||
|
||||
item.organization?.toLowerCase().includes(searchText)
|
||||
);
|
||||
}
|
||||
|
||||
if (filters.value.vehicleStatus) {
|
||||
filtered = filtered.filter(item => item.status === filters.value.vehicleStatus);
|
||||
}
|
||||
|
||||
if (filters.value.vehicleType) {
|
||||
filtered = filtered.filter(item => item.type === filters.value.vehicleType);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
});
|
||||
|
||||
// 获取行驶状态对应的类型
|
||||
function getRouteStatusType(status) {
|
||||
switch (status) {
|
||||
case '任务中':
|
||||
return 'success';
|
||||
case '待命中':
|
||||
return '';
|
||||
case '充电中':
|
||||
return 'warning';
|
||||
case '等待中':
|
||||
return 'info';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
// 更新过滤条件
|
||||
function updateFilters(newFilters) {
|
||||
filters.value = { ...filters.value, ...newFilters };
|
||||
}
|
||||
|
||||
// 重置过滤条件
|
||||
function resetFilters() {
|
||||
filters.value = {
|
||||
searchText: '',
|
||||
vehicleType: '',
|
||||
vehicleStatus: ''
|
||||
};
|
||||
}
|
||||
|
||||
// 导出数据
|
||||
function exportData() {
|
||||
// 实现导出逻辑
|
||||
console.log('导出数据', filteredVehicleData.value);
|
||||
}
|
||||
|
||||
return {
|
||||
vehicleData,
|
||||
vehicleStats,
|
||||
filters,
|
||||
filteredVehicleData,
|
||||
getRouteStatusType,
|
||||
updateFilters,
|
||||
resetFilters,
|
||||
exportData
|
||||
};
|
||||
}
|
||||
@ -22,7 +22,7 @@
|
||||
</el-menu-item>
|
||||
|
||||
<!-- 带子菜单的菜单项 -->
|
||||
<el-sub-menu v-else :index="route.path || ''">
|
||||
<el-sub-menu v-else :index="route.path || ''" :class="{'is-active': isParentActive(route)}">
|
||||
<template #title>
|
||||
<el-icon><svg-icon :icon-class="route.meta?.icon || ''" /></el-icon>
|
||||
<span class="menu-title">{{ route.meta?.title || '未命名菜单' }}</span>
|
||||
@ -165,6 +165,16 @@ function findMenuItemByKey(key) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 判断父菜单是否应该高亮
|
||||
const isParentActive = (route) => {
|
||||
if (!route || !route.children) return false;
|
||||
const currentPath = activeSubmenu.value;
|
||||
return route.children.some(child => {
|
||||
const fullPath = getFullPath(route.path, child.path);
|
||||
return currentPath === fullPath;
|
||||
});
|
||||
};
|
||||
|
||||
// 初始化时设置当前激活的子菜单
|
||||
onMounted(() => {
|
||||
const currentPath = route.path;
|
||||
@ -281,4 +291,29 @@ watch(() => permissionStore.sidebarRouters, (newRoutes) => {
|
||||
.timeline-dot.active {
|
||||
background-color: #1890ff;
|
||||
}
|
||||
|
||||
/* 父菜单激活状态样式 */
|
||||
:deep(.el-sub-menu__title.is-active) {
|
||||
background-color: #343744 !important;
|
||||
border-radius: 10px!important;
|
||||
|
||||
}
|
||||
|
||||
:deep(.el-sub-menu.is-active .el-sub-menu__title) {
|
||||
background-color: #343744 !important;
|
||||
border-radius: 10px!important;
|
||||
|
||||
}
|
||||
|
||||
/* 确保子菜单容器背景色与父菜单一致 */
|
||||
:deep(.el-menu--inline) {
|
||||
background-color: #292C38 !important;
|
||||
border-radius: 10px!important;
|
||||
}
|
||||
|
||||
/* 父菜单hover效果 */
|
||||
:deep(.el-sub-menu .el-sub-menu__title:hover) {
|
||||
background-color: #343744 !important;
|
||||
border-radius: 10px!important;
|
||||
}
|
||||
</style>
|
||||
|
||||
344
src/views/car/monitor/detail.vue
Normal file
@ -0,0 +1,344 @@
|
||||
<!-- 车辆详情内容组件 -->
|
||||
<template>
|
||||
<div class="vehicle-detail-content">
|
||||
<!-- 上部分 -->
|
||||
<div class="detail-header">
|
||||
<!-- 返回按钮 - 添加在右上角 -->
|
||||
<div class="back-button">
|
||||
<el-button type="primary" plain class="btn custom-back-btn" size="small" @click="$emit('close')">
|
||||
<el-icon class="back-icon"><ArrowLeft /></el-icon>
|
||||
返回列表
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 左侧车辆图片 -->
|
||||
<div class="vehicle-image">
|
||||
<img :src="carPic" alt="车辆图片" />
|
||||
</div>
|
||||
<!-- 右侧车辆信息 -->
|
||||
<div class="vehicle-info-panel">
|
||||
<div class="info-top-row">
|
||||
<img :src="carIcon" class="car-icon" />
|
||||
</div>
|
||||
<div class="info-bottom-row">
|
||||
<div class="info-title">{{ vehicle?.carId || "QN001" }}</div>
|
||||
<div class="info-status-row">
|
||||
<span class="status-item">
|
||||
<span class="status-label">作业状态:</span>
|
||||
<span class="dot status-signal"></span>
|
||||
<span class="status-value status-task">{{ vehicle?.routeStatus || "任务中" }}</span>
|
||||
</span>
|
||||
<span class="status-sep">|</span>
|
||||
<span class="status-item">
|
||||
<span class="dot status-signal"></span>
|
||||
<span class="status-label">车辆状态:</span>
|
||||
<span class="status-value status-online">{{ vehicle?.status || "在线" }}</span>
|
||||
</span>
|
||||
<span class="status-sep">|</span>
|
||||
<span class="status-item">
|
||||
<span class="dot status-signal"></span>
|
||||
<span class="status-value">信号良好</span>
|
||||
</span>
|
||||
<span class="status-sep">|</span>
|
||||
<span class="status-item">
|
||||
<span class="dot status-battery"></span>
|
||||
<span class="status-value">电量充足</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="info-cards-row">
|
||||
<div class="info-card" v-for="item in cardList" :key="item.name">
|
||||
<div class="card-icon">
|
||||
<img :src="item.icon" />
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="card-title">{{ item.name }}</div>
|
||||
<div class="card-value">{{ item.value }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 下部分:标签页 -->
|
||||
<div class="detail-tabs">
|
||||
<el-tabs v-model="activeTab" class="demo-tabs" @tab-click="handleClick">
|
||||
<el-tab-pane label="电池概况" name="battery">
|
||||
<BatteryOverview :vehicle="vehicle" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="故障报警" name="fault">
|
||||
<FaultAlarm :vehicle="vehicle" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="轨迹回放" name="track">
|
||||
<TrackPlayback :vehicle="vehicle" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="充放电统计" name="charging">
|
||||
<ChargingStats :vehicle="vehicle" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="视频监控" name="video">
|
||||
<VideoMonitoring :vehicle="vehicle" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, defineProps, onMounted } from "vue";
|
||||
import { ArrowLeft } from '@element-plus/icons-vue';
|
||||
import BatteryOverview from "../../../components/car/detail/BatteryOverview.vue";
|
||||
import FaultAlarm from "../../../components/car/detail/FaultAlarm.vue";
|
||||
import TrackPlayback from "../../../components/car/detail/TrackPlayback.vue";
|
||||
import ChargingStats from "../../../components/car/detail/ChargingStats.vue";
|
||||
import VideoMonitoring from "../../../components/car/detail/VideoMonitoring.vue";
|
||||
import carPic from "../../../assets/images/car.png";
|
||||
import carIcon from "../../../assets/images/carIcon.png";
|
||||
import carIcon1 from "../../../assets/images/carIcon1.png";
|
||||
import carIcon2 from "../../../assets/images/carIcon2.png";
|
||||
import carIcon3 from "../../../assets/images/carIcon3.png";
|
||||
import carIcon4 from "../../../assets/images/carIcon4.png";
|
||||
import carIcon5 from "../../../assets/images/carIcon5.png";
|
||||
// import phoneIcon from '../../../assets/images/phone.png';
|
||||
const props = defineProps({
|
||||
vehicle: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
const activeTab = ref("battery");
|
||||
const cardList = [
|
||||
{ name: "驱动车", value: props.vehicle?.type || "车型类型", icon: carIcon1 },
|
||||
{ name: "比亚迪", value: props.vehicle?.brand || "出场品牌", icon: carIcon2 },
|
||||
{
|
||||
name: "物流建筑公司",
|
||||
value: props.vehicle?.organization || "所属单位",
|
||||
icon: carIcon5,
|
||||
},
|
||||
{
|
||||
name: "张三",
|
||||
value: props.vehicle?.batteryLevel || "负责人",
|
||||
icon: carIcon4,
|
||||
},
|
||||
{
|
||||
name: props.vehicle?.contactPhone || "15697423956",
|
||||
value: "负责人电话",
|
||||
icon: carIcon3,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.vehicle-detail-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.detail-header {
|
||||
display: flex;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 8px;
|
||||
background-color: #292c38;
|
||||
padding: 24px;
|
||||
justify-content: flex-start;
|
||||
gap:20px;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 返回按钮样式 */
|
||||
.back-button {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* 自定义返回按钮样式 */
|
||||
.custom-back-btn {
|
||||
background-color: rgba(52, 122, 226, 0.2) !important;
|
||||
border-color: #347AE2 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.custom-back-btn:hover {
|
||||
background-color: rgba(52, 122, 226, 0.3) !important;
|
||||
}
|
||||
|
||||
.back-icon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.vehicle-image {
|
||||
width: 255px;
|
||||
height: 154px;
|
||||
border-radius: 8px;
|
||||
/* overflow: hidden; */
|
||||
background-color: #1e2233;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
}
|
||||
.vehicle-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.vehicle-info-panel {
|
||||
|
||||
/* width: 60%; */
|
||||
flex: 1;
|
||||
display: flex;
|
||||
/* flex-direction: column; */
|
||||
justify-content: center;
|
||||
}
|
||||
.info-bottom-row{
|
||||
width: 100%;
|
||||
|
||||
}
|
||||
.info-top-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
/* margin-bottom: 8px; */
|
||||
margin-top: 10px;
|
||||
|
||||
}
|
||||
.car-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.info-title {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.info-status-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
margin-bottom: 16px;
|
||||
font-size: 15px;
|
||||
}
|
||||
.status-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.status-label {
|
||||
color: #b0b8c9;
|
||||
margin-right: 2px;
|
||||
}
|
||||
.status-value {
|
||||
/* font-weight: bold; */
|
||||
margin-right: 2px;
|
||||
color: #fff;
|
||||
}
|
||||
.status-task {
|
||||
color: #409eff;
|
||||
}
|
||||
.status-online {
|
||||
color: #00c48f;
|
||||
}
|
||||
.status-signal {
|
||||
background: #4ea1ff;
|
||||
}
|
||||
.status-battery {
|
||||
background: #00c48f;
|
||||
}
|
||||
.status-sep {
|
||||
color: #b0b8c9;
|
||||
margin: 0 12px;
|
||||
font-weight: normal;
|
||||
font-size: 15px;
|
||||
}
|
||||
.dot {
|
||||
display: inline-block;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
margin-right: 4px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.info-cards-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
|
||||
}
|
||||
.info-card {
|
||||
/* width: 100%; */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #343744;
|
||||
border-radius: 8px;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
.card-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.card-icon img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
.card-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
.card-title {
|
||||
font-size: 14px;
|
||||
color: #b0b8c9;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.card-value {
|
||||
font-size: 12px;
|
||||
color: #fff;
|
||||
}
|
||||
.detail-tabs {
|
||||
flex: 1;
|
||||
padding:12px 24px;
|
||||
background-color: #292C38;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
:deep(.el-tabs) {
|
||||
--el-tabs-header-background-color: #1e2233;
|
||||
--el-tabs-border-color: #303850;
|
||||
}
|
||||
:deep(.el-tabs__header) {
|
||||
margin-bottom: 20px;
|
||||
border-bottom: none;
|
||||
}
|
||||
:deep(.el-tabs__item) {
|
||||
color: #F0F0F0;
|
||||
}
|
||||
:deep(.el-tabs__item.is-active) {
|
||||
color: #409eff;
|
||||
}
|
||||
::v-deep .el-tabs__nav-wrap::after {
|
||||
height: 1px;
|
||||
background-color: #4C4F5F;
|
||||
}
|
||||
@media (max-width: 992px) {
|
||||
.detail-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.vehicle-image {
|
||||
margin-right: 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.info-cards-row {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,456 +1,206 @@
|
||||
<!-- 车辆监控内容组件 -->
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<!-- 车辆状态卡片 -->
|
||||
<div class="status-cards">
|
||||
<el-card class="status-card online" shadow="hover">
|
||||
<div class="card-header">
|
||||
<div class="card-title">在线</div>
|
||||
<div class="card-value">{{ statistics.online }}</div>
|
||||
<div class="monitor_container">
|
||||
<!-- 使用过渡效果实现平滑切换 -->
|
||||
<transition name="fade" mode="out-in">
|
||||
<!-- 根据showDetail状态显示不同的内容 -->
|
||||
<div v-if="!showDetail" key="list-view">
|
||||
<!-- 顶部统计信息 -->
|
||||
<VehicleStats
|
||||
v-model="activeStatIndex"
|
||||
:stats="vehicleStats"
|
||||
class="stats-section"
|
||||
/>
|
||||
|
||||
<!-- 表格区域 -->
|
||||
<div class="app-container">
|
||||
<VehicleTable
|
||||
:vehicleData="filteredVehicleData"
|
||||
:filters="filters"
|
||||
@view="viewVehicleDetail"
|
||||
@control="controlVehicle"
|
||||
@selection-change="handleSelectionChange"
|
||||
@filter="handleFilter"
|
||||
@reset="resetFilter"
|
||||
@export="exportVehicleData"
|
||||
/>
|
||||
</div>
|
||||
<div class="card-indicator">
|
||||
<span class="percentage">+30%</span>
|
||||
<i class="el-icon-arrow-up"></i>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card class="status-card task" shadow="hover">
|
||||
<div class="card-header">
|
||||
<div class="card-title">任务中</div>
|
||||
<div class="card-value">{{ statistics.inTask }}</div>
|
||||
</div>
|
||||
<div class="card-indicator">
|
||||
<span class="percentage">+20%</span>
|
||||
<i class="el-icon-arrow-up"></i>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card class="status-card error" shadow="hover">
|
||||
<div class="card-header">
|
||||
<div class="card-title">故障</div>
|
||||
<div class="card-value">{{ statistics.error }}</div>
|
||||
</div>
|
||||
<div class="card-indicator">
|
||||
<span class="percentage">-10%</span>
|
||||
<i class="el-icon-arrow-down"></i>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card class="status-card offline" shadow="hover">
|
||||
<div class="card-header">
|
||||
<div class="card-title">离线</div>
|
||||
<div class="card-value">{{ statistics.offline }}</div>
|
||||
</div>
|
||||
<div class="card-indicator">
|
||||
<span class="percentage">-5%</span>
|
||||
<i class="el-icon-arrow-down"></i>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<div class="search-bar">
|
||||
<div class="search-input">
|
||||
<el-input
|
||||
v-model="searchQuery"
|
||||
placeholder="请输入车辆号或者司机"
|
||||
prefix-icon="Search"
|
||||
@keyup.enter="handleSearch"
|
||||
>
|
||||
</el-input>
|
||||
</div>
|
||||
|
||||
<el-select v-model="vehicleState" placeholder="车辆状态" class="search-select">
|
||||
<el-option label="全部" value=""></el-option>
|
||||
<el-option label="在线" value="online"></el-option>
|
||||
<el-option label="离线" value="offline"></el-option>
|
||||
<el-option label="故障" value="error"></el-option>
|
||||
</el-select>
|
||||
|
||||
<el-select v-model="operationState" placeholder="运行状态" class="search-select">
|
||||
<el-option label="全部" value=""></el-option>
|
||||
<el-option label="任务中" value="running"></el-option>
|
||||
<el-option label="空闲中" value="idle"></el-option>
|
||||
<el-option label="充电中" value="charging"></el-option>
|
||||
<el-option label="维保中" value="maintenance"></el-option>
|
||||
</el-select>
|
||||
|
||||
<div class="search-buttons">
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
<el-button @click="resetSearch">重置</el-button>
|
||||
<!-- 详情视图 - 完全替换上面的内容 -->
|
||||
<div v-else key="detail-view" class="detail-view">
|
||||
<VehicleDetail
|
||||
:vehicleData="currentVehicle"
|
||||
@close="closeDetail"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="view-buttons">
|
||||
<el-button :icon="List"></el-button>
|
||||
<el-button :icon="Grid"></el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 车辆表格 -->
|
||||
<el-table
|
||||
:data="vehicleList"
|
||||
border
|
||||
stripe
|
||||
style="width: 100%"
|
||||
@selection-change="handleSelectionChange"
|
||||
:header-cell-style="{background:'#292c38',color:'#fff'}"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column prop="id" label="序号" width="80" />
|
||||
<el-table-column prop="name" label="车辆" width="100">
|
||||
<template #default="scope">
|
||||
<span class="vehicle-link">{{ scope.row.name }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="vehicleNo" label="车辆号" width="100" />
|
||||
<el-table-column prop="driverName" label="驾驶员" width="100" />
|
||||
<el-table-column prop="vehicleType" label="特性值" width="100" />
|
||||
<el-table-column prop="company" label="所属管理公司" width="150" />
|
||||
<el-table-column prop="seats" label="座位数" width="80" />
|
||||
<el-table-column prop="taskStatus" label="任务状态" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getTaskStatusType(scope.row.taskStatus)">{{ scope.row.taskStatus }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="runStatus" label="在线状态" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getRunStatusType(scope.row.runStatus)">{{ scope.row.runStatus }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="parkStatus" label="停车状态" width="100" />
|
||||
<el-table-column prop="voltage" label="电压" width="100" />
|
||||
<el-table-column prop="current" label="电流" width="100" />
|
||||
<el-table-column prop="battery" label="电量" width="100" />
|
||||
<el-table-column prop="updateTime" label="数据更新时间" width="180" />
|
||||
<el-table-column prop="temperature" label="T3机器" width="100" />
|
||||
<el-table-column prop="status" label="状态" width="80" />
|
||||
<el-table-column prop="deviceId" label="设备标识码" width="150" />
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container">
|
||||
<el-pagination
|
||||
v-model:currentPage="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
:page-sizes="[10, 20, 30, 50]"
|
||||
layout="total, prev, pager, next, jumper"
|
||||
:total="total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
background
|
||||
/>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="CarMonitor">
|
||||
import { ref, onMounted, reactive } from 'vue';
|
||||
import { Search, List, Grid } from '@element-plus/icons-vue';
|
||||
<script setup>
|
||||
import { ref, defineEmits } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { Close } from '@element-plus/icons-vue';
|
||||
import VehicleStats from '../../../components/car/VehicleStats.vue';
|
||||
import VehicleTable from '../../../components/car/VehicleTable.vue';
|
||||
import VehicleDetail from './detail.vue';
|
||||
import { useVehicle } from '../../../composables/useVehicle';
|
||||
|
||||
// 统计数据
|
||||
const statistics = reactive({
|
||||
online: 85,
|
||||
inTask: 57,
|
||||
error: 4,
|
||||
offline: 12
|
||||
});
|
||||
const emit = defineEmits(['edit-vehicle', 'view-vehicle']);
|
||||
|
||||
// 搜索条件
|
||||
const searchQuery = ref('');
|
||||
const vehicleState = ref('');
|
||||
const operationState = ref('');
|
||||
// 使用车辆组合式函数
|
||||
const {
|
||||
vehicleData,
|
||||
vehicleStats,
|
||||
filters,
|
||||
filteredVehicleData,
|
||||
updateFilters,
|
||||
resetFilters,
|
||||
exportData
|
||||
} = useVehicle();
|
||||
|
||||
// 分页参数
|
||||
const currentPage = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const total = ref(100);
|
||||
// 统计数据选中索引
|
||||
const activeStatIndex = ref(0);
|
||||
|
||||
// 表格数据
|
||||
const vehicleList = ref([
|
||||
{
|
||||
id: 1,
|
||||
name: '蓝奇',
|
||||
vehicleNo: 'QN001',
|
||||
driverName: '驾驶员1',
|
||||
vehicleType: '电动',
|
||||
company: '青岛管理公司',
|
||||
seats: 3,
|
||||
taskStatus: '任务中',
|
||||
runStatus: '在线',
|
||||
parkStatus: '停车充电',
|
||||
voltage: '360V',
|
||||
current: '200A',
|
||||
battery: '90%',
|
||||
updateTime: '2023-06-17 15:48:30',
|
||||
temperature: '正常',
|
||||
status: '正常',
|
||||
deviceId: '123456789001'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '蓝奇',
|
||||
vehicleNo: 'QN002',
|
||||
driverName: '驾驶员2',
|
||||
vehicleType: '电动',
|
||||
company: '青岛管理公司',
|
||||
seats: 3,
|
||||
taskStatus: '任务中',
|
||||
runStatus: '在线',
|
||||
parkStatus: '停车充电',
|
||||
voltage: '360V',
|
||||
current: '200A',
|
||||
battery: '85%',
|
||||
updateTime: '2023-06-17 15:45:30',
|
||||
temperature: '正常',
|
||||
status: '正常',
|
||||
deviceId: '123456789002'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '蓝奇',
|
||||
vehicleNo: 'QN003',
|
||||
driverName: '驾驶员3',
|
||||
vehicleType: '电动',
|
||||
company: '青岛管理公司',
|
||||
seats: 3,
|
||||
taskStatus: '任务中',
|
||||
runStatus: '在线',
|
||||
parkStatus: '停车充电',
|
||||
voltage: '360V',
|
||||
current: '200A',
|
||||
battery: '75%',
|
||||
updateTime: '2023-06-17 15:40:30',
|
||||
temperature: '正常',
|
||||
status: '正常',
|
||||
deviceId: '123456789003'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '蓝奇',
|
||||
vehicleNo: 'QN004',
|
||||
driverName: '驾驶员4',
|
||||
vehicleType: '电动',
|
||||
company: '青岛管理公司',
|
||||
seats: 3,
|
||||
taskStatus: '闲置中',
|
||||
runStatus: '离线',
|
||||
parkStatus: '停车充电',
|
||||
voltage: '360V',
|
||||
current: '200A',
|
||||
battery: '95%',
|
||||
updateTime: '2023-06-17 15:30:30',
|
||||
temperature: '正常',
|
||||
status: '正常',
|
||||
deviceId: '123456789004'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: '蓝奇',
|
||||
vehicleNo: 'QN005',
|
||||
driverName: '驾驶员5',
|
||||
vehicleType: '电动',
|
||||
company: '青岛管理公司',
|
||||
seats: 3,
|
||||
taskStatus: '闲置中',
|
||||
runStatus: '离线',
|
||||
parkStatus: '停车充电',
|
||||
voltage: '360V',
|
||||
current: '200A',
|
||||
battery: '100%',
|
||||
updateTime: '2023-06-17 15:20:30',
|
||||
temperature: '正常',
|
||||
status: '正常',
|
||||
deviceId: '123456789005'
|
||||
}
|
||||
]);
|
||||
// 地图相关
|
||||
const showMap = ref(false);
|
||||
|
||||
// 选中的行
|
||||
// 表格选中行
|
||||
const selectedRows = ref([]);
|
||||
|
||||
// 方法
|
||||
function handleSearch() {
|
||||
// 实际应用中这里会调用API获取数据
|
||||
console.log('Search with:', {
|
||||
query: searchQuery.value,
|
||||
vehicleState: vehicleState.value,
|
||||
operationState: operationState.value
|
||||
// 详情显示控制
|
||||
const showDetail = ref(false);
|
||||
const currentVehicle = ref(null);
|
||||
|
||||
// 切换地图显示
|
||||
function toggleMap() {
|
||||
showMap.value = !showMap.value;
|
||||
}
|
||||
|
||||
// 查看车辆详情 - 点击查看按钮时触发
|
||||
function viewVehicleDetail(vehicle) {
|
||||
currentVehicle.value = vehicle;
|
||||
showDetail.value = true;
|
||||
}
|
||||
|
||||
// 关闭详情 - 返回列表视图
|
||||
function closeDetail() {
|
||||
showDetail.value = false;
|
||||
}
|
||||
|
||||
// 远程控制车辆
|
||||
function controlVehicle(vehicle) {
|
||||
ElMessageBox.confirm(
|
||||
`确定要对 ${vehicle.id} 进行远程控制吗?`,
|
||||
'警告',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: `已发送控制指令到车辆 ${vehicle.id}`,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage({
|
||||
type: 'info',
|
||||
message: '已取消操作',
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 编辑车辆信息
|
||||
function editVehicle(vehicle) {
|
||||
emit('edit-vehicle', vehicle);
|
||||
}
|
||||
|
||||
// 处理表格选择变化
|
||||
function handleSelectionChange(rows) {
|
||||
selectedRows.value = rows;
|
||||
}
|
||||
|
||||
// 处理搜索
|
||||
function handleSearch(keyword) {
|
||||
// 实现搜索逻辑
|
||||
ElMessage({
|
||||
message: `搜索关键词: ${keyword}`,
|
||||
type: 'success'
|
||||
});
|
||||
// 重置到第一页
|
||||
currentPage.value = 1;
|
||||
}
|
||||
|
||||
function resetSearch() {
|
||||
searchQuery.value = '';
|
||||
vehicleState.value = '';
|
||||
operationState.value = '';
|
||||
handleSearch();
|
||||
// 处理过滤
|
||||
function handleFilter(filterValues) {
|
||||
updateFilters(filterValues);
|
||||
}
|
||||
|
||||
function handleSelectionChange(selection) {
|
||||
selectedRows.value = selection;
|
||||
// 重置过滤器
|
||||
function resetFilter() {
|
||||
resetFilters();
|
||||
}
|
||||
|
||||
function handleSizeChange(val) {
|
||||
pageSize.value = val;
|
||||
// 实际应用中这里会重新获取数据
|
||||
// 导出数据
|
||||
function exportVehicleData() {
|
||||
exportData();
|
||||
}
|
||||
|
||||
function handleCurrentChange(val) {
|
||||
currentPage.value = val;
|
||||
// 实际应用中这里会重新获取数据
|
||||
}
|
||||
|
||||
function getTaskStatusType(status) {
|
||||
if (status === '任务中') return 'success';
|
||||
if (status === '闲置中') return 'warning';
|
||||
if (status === '等待中') return 'info';
|
||||
return 'info';
|
||||
}
|
||||
|
||||
function getRunStatusType(status) {
|
||||
if (status === '在线') return 'success';
|
||||
if (status === '离线') return 'warning';
|
||||
if (status === '故障') return 'danger';
|
||||
return 'info';
|
||||
}
|
||||
|
||||
// 加载初始数据
|
||||
onMounted(() => {
|
||||
// 实际应用中这里会调用API获取数据
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.app-container {
|
||||
padding: 20px;
|
||||
background-color: #1e2028;
|
||||
color: #ffffff;
|
||||
.monitor_container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.status-cards {
|
||||
display: flex;
|
||||
.stats-section {
|
||||
margin-bottom: 20px;
|
||||
gap: 20px;
|
||||
|
||||
.status-card {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
background-color: #292c38;
|
||||
border: none;
|
||||
|
||||
&.online {
|
||||
.card-header {
|
||||
border-bottom: 2px solid #67c23a;
|
||||
}
|
||||
}
|
||||
|
||||
&.task {
|
||||
.card-header {
|
||||
border-bottom: 2px solid #409eff;
|
||||
}
|
||||
}
|
||||
|
||||
&.error {
|
||||
.card-header {
|
||||
border-bottom: 2px solid #f56c6c;
|
||||
}
|
||||
}
|
||||
|
||||
&.offline {
|
||||
.card-header {
|
||||
border-bottom: 2px solid #e6a23c;
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-bottom: 10px;
|
||||
|
||||
.card-title {
|
||||
font-size: 14px;
|
||||
color: #96a0b5;
|
||||
}
|
||||
|
||||
.card-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.card-indicator {
|
||||
font-size: 12px;
|
||||
margin-top: 10px;
|
||||
color: #67c23a;
|
||||
|
||||
.percentage {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
.app-container {
|
||||
padding: 20px;
|
||||
background-color: #292C38;
|
||||
color: #ffffff;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
margin-bottom: 20px;
|
||||
gap: 15px;
|
||||
align-items: center;
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.detail-view {
|
||||
height: 100%;
|
||||
min-height: calc(100vh - 100px);
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
/* 淡入淡出过渡效果 */
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.search-select {
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
.search-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.view-buttons {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table) {
|
||||
background-color: #292c38;
|
||||
color: #fff;
|
||||
|
||||
.el-table__header-wrapper th {
|
||||
background-color: #292c38;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.el-table__row {
|
||||
background-color: #292c38;
|
||||
&:hover > td {
|
||||
background-color: #363945 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-table__row--striped td {
|
||||
background-color: #1e2028;
|
||||
}
|
||||
}
|
||||
|
||||
// .pagination-container {
|
||||
// display: flex;
|
||||
// justify-content: flex-end;
|
||||
// margin-top: 20px;
|
||||
// }
|
||||
|
||||
.vehicle-link {
|
||||
color: #409eff;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.detail-view {
|
||||
min-height: calc(100vh - 80px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,6 +1,9 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<div class="park-container">
|
||||
<!-- 车辆状态统计卡片 -->
|
||||
<VehicleStats :stats="vehicleStats" />
|
||||
|
||||
<!-- 搜索区域 -->
|
||||
<div class="search-area">
|
||||
<el-input
|
||||
@ -12,10 +15,17 @@
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
|
||||
<el-select v-model="queryParams.status" placeholder="车辆类型:全部" clearable class="search-select">
|
||||
<el-select v-model="queryParams.type" placeholder="车辆类型" clearable class="search-select">
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="在库" value="0" />
|
||||
<el-option label="离库" value="1" />
|
||||
<el-option label="驱动车" value="驱动车" />
|
||||
<el-option label="巡检车" value="巡检车" />
|
||||
</el-select>
|
||||
|
||||
<el-select v-model="queryParams.status" placeholder="车辆状态" clearable class="search-select">
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="在线" value="在线" />
|
||||
<el-option label="离线" value="离线" />
|
||||
<el-option label="故障" value="故障" />
|
||||
</el-select>
|
||||
|
||||
<div class="search-buttons">
|
||||
@ -24,17 +34,14 @@
|
||||
</div>
|
||||
|
||||
<div class="action-buttons">
|
||||
<el-button type="primary" class="search-btn" @click="handleAdd">新增</el-button>
|
||||
<el-button class="reset-btn" plain @click="handleImport">导入</el-button>
|
||||
<el-button class="reset-btn" plain @click="handleExport">导出</el-button>
|
||||
<el-button class="export-btn" type="info" :disabled="multiple" @click="handleDelete">删除</el-button>
|
||||
<el-button class="export-btn" @click="handleExport">导出</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 车辆表格 -->
|
||||
<!-- 车辆监控表格 -->
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="parkList"
|
||||
:data="vehicleList"
|
||||
@selection-change="handleSelectionChange"
|
||||
style="width: 100%"
|
||||
class="custom-table"
|
||||
@ -49,23 +56,34 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120" align="left">
|
||||
<template #default="scope">
|
||||
<el-button link text @click="handleUpdate(scope.row)">编辑</el-button>
|
||||
<el-button link text type="primary" @click="handleDelete(scope.row)">删除</el-button>
|
||||
<el-button link text @click="handleView(scope.row)">查看</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="车牌号" prop="carNo" align="left" />
|
||||
<el-table-column label="类型" prop="carType" align="left" />
|
||||
<el-table-column label="品牌" prop="carBrand" align="left" />
|
||||
|
||||
<el-table-column label="所属单位" prop="company" align="left" />
|
||||
<el-table-column label="车辆号" prop="carId" align="left" />
|
||||
<el-table-column label="类型" prop="type" align="left" />
|
||||
<el-table-column label="品牌" prop="brand" align="left" />
|
||||
<el-table-column label="所属单位" prop="organization" align="left" />
|
||||
<el-table-column label="当前故障数" prop="faultCount" align="left" />
|
||||
<el-table-column label="作业状态" align="left">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getRouteStatusType(scope.row.routeStatus)">
|
||||
{{ scope.row.routeStatus }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="车辆状态" align="left">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getStatusType(scope.row.status)">
|
||||
{{ scope.row.status }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="充电状态" prop="chargeStatus" align="left" />
|
||||
<el-table-column label="电池电量" prop="batteryLevel" align="left" />
|
||||
<el-table-column label="最新位置" prop="location" align="left" />
|
||||
<el-table-column label="最新时间" prop="lastUpdateTime" align="left" width="160" />
|
||||
<el-table-column label="负责人" prop="contact" align="left" />
|
||||
<el-table-column label="负责人电话" prop="phonenumber" align="left" />
|
||||
<el-table-column label="创建人" prop="creator" align="left" />
|
||||
<el-table-column label="创建时间" align="left" prop="createTime" width="160">
|
||||
<template #default="scope">
|
||||
<span>{{ parseTime(scope.row.createTime) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页组件 -->
|
||||
@ -77,279 +95,201 @@
|
||||
@pagination="getList"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 添加或修改车辆对话框 -->
|
||||
<el-dialog :title="title" v-model="open" width="600px" append-to-body>
|
||||
<el-form :model="form" :rules="rules" ref="parkRef" label-width="100px">
|
||||
<el-row>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="车牌号" prop="carNo">
|
||||
<el-input
|
||||
v-model="form.carNo"
|
||||
placeholder="请输入车牌号"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="车辆类型" prop="carType">
|
||||
<el-input
|
||||
v-model="form.carType"
|
||||
placeholder="请输入车辆类型"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="车辆品牌" prop="carBrand">
|
||||
<el-input
|
||||
v-model="form.carBrand"
|
||||
placeholder="请输入车辆品牌"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="所属单位" prop="company">
|
||||
<el-input
|
||||
v-model="form.company"
|
||||
placeholder="请输入所属单位"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="负责人" prop="contact">
|
||||
<el-input
|
||||
v-model="form.contact"
|
||||
placeholder="请输入负责人"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="负责人电话" prop="phonenumber">
|
||||
<el-input
|
||||
v-model="form.phonenumber"
|
||||
placeholder="请输入负责人电话"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="车辆图片">
|
||||
<div class="avatar-uploader-box">
|
||||
<div v-if="form.avatar" class="avatar-preview">
|
||||
<img :src="form.avatar" class="avatar" />
|
||||
<div class="avatar-count">1/1</div>
|
||||
<div class="avatar-replace" @click.stop="handleRemoveAvatar">
|
||||
<el-icon><Close /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<el-upload
|
||||
v-else
|
||||
class="avatar-uploader"
|
||||
:show-file-list="false"
|
||||
:action="upload.avatarUrl"
|
||||
:headers="upload.headers"
|
||||
:before-upload="beforeAvatarUpload"
|
||||
:on-success="handleAvatarSuccess"
|
||||
>
|
||||
<div class="avatar-upload-placeholder">
|
||||
<el-icon class="avatar-uploader-icon"><Plus /></el-icon>
|
||||
<div class="avatar-count">0/1</div>
|
||||
</div>
|
||||
</el-upload>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button class="export-btn" @click="cancel">取 消</el-button>
|
||||
<el-button class="search-btn" type="primary" @click="submitForm">确 定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 车辆导入对话框 -->
|
||||
<el-dialog :title="upload.title" v-model="upload.open" width="500px" append-to-body class="import-dialog">
|
||||
<div class="import-title">选择需要导入的项目文件</div>
|
||||
<el-upload
|
||||
ref="uploadRef"
|
||||
:limit="1"
|
||||
accept=".xlsx, .xls"
|
||||
:headers="upload.headers"
|
||||
:action="upload.url + '?updateSupport=' + upload.updateSupport"
|
||||
:disabled="upload.isUploading"
|
||||
:on-progress="handleFileUploadProgress"
|
||||
:on-success="handleFileSuccess"
|
||||
:auto-upload="false"
|
||||
drag
|
||||
class="import-upload"
|
||||
>
|
||||
<div class="upload-content">
|
||||
<el-icon class="upload-icon"><upload-filled /></el-icon>
|
||||
<div class="upload-text">拖拽文件到这里,或</div>
|
||||
<div class="upload-link">
|
||||
<el-icon class="upload-icon-small" style="color: #347AE2"><upload-filled /></el-icon>
|
||||
<span class="click-upload">点此添加</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-upload>
|
||||
<div class="import-footer">
|
||||
<div class="footer-left">
|
||||
<el-checkbox v-model="upload.updateSupport">是否更新已经存在的车辆数据</el-checkbox>
|
||||
</div>
|
||||
<div class="footer-right">
|
||||
<span class="file-format">仅允许导入xls、xlsx格式文件。</span>
|
||||
<el-link type="primary" :underline="false" class="download-link" @click="importTemplate">下载模板</el-link>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button class="export-btn" @click="upload.open = false">取 消</el-button>
|
||||
<el-button class="search-btn" type="primary" @click="submitFileForm">确 定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="Park">
|
||||
<script setup name="VehicleMonitor">
|
||||
import { ref, reactive, onMounted, computed } from 'vue';
|
||||
import { getToken } from "@/utils/auth";
|
||||
import { parseTime } from "@/utils/ruoyi";
|
||||
import { UploadFilled, Plus, Close } from '@element-plus/icons-vue';
|
||||
import { Search, Download } from '@element-plus/icons-vue';
|
||||
import VehicleStats from '@/components/car/VehicleStats.vue';
|
||||
import Pagination from '@/components/Pagination/index.vue';
|
||||
|
||||
const { proxy } = getCurrentInstance();
|
||||
|
||||
const parkList = ref([]);
|
||||
const open = ref(false);
|
||||
const vehicleList = ref([]);
|
||||
const loading = ref(true);
|
||||
const ids = ref([]);
|
||||
const single = ref(true);
|
||||
const multiple = ref(true);
|
||||
const total = ref(0);
|
||||
const title = ref("");
|
||||
|
||||
// 模拟数据
|
||||
const mockParkData = [
|
||||
{
|
||||
id: 1,
|
||||
carNo: 'QH001',
|
||||
carType: '物流车',
|
||||
carBrand: '大众',
|
||||
company: '青岛机场物流公司',
|
||||
contact: '张三',
|
||||
phonenumber: '12345678920',
|
||||
creator: '王管理',
|
||||
createTime: '2024-04-17 10:48:30',
|
||||
carImage: ''
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
carNo: 'QH002',
|
||||
carType: '客车',
|
||||
carBrand: '金龙',
|
||||
company: '青岛机场运输公司',
|
||||
contact: '李四',
|
||||
phonenumber: '12345678921',
|
||||
creator: '王管理',
|
||||
createTime: '2024-04-17 10:48:30',
|
||||
carImage: ''
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
carNo: 'QH003',
|
||||
carType: '货车',
|
||||
carBrand: '解放',
|
||||
company: '青岛机场货运部',
|
||||
contact: '王五',
|
||||
phonenumber: '12345678922',
|
||||
creator: '王管理',
|
||||
createTime: '2024-04-17 10:48:30',
|
||||
carImage: ''
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
carNo: 'QH004',
|
||||
carType: '物流车',
|
||||
carBrand: '福特',
|
||||
company: '青岛机场物流公司',
|
||||
contact: '赵六',
|
||||
phonenumber: '12345678923',
|
||||
creator: '王管理',
|
||||
createTime: '2024-04-17 10:48:30',
|
||||
carImage: ''
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
carNo: 'QH005',
|
||||
carType: '客车',
|
||||
carBrand: '宇通',
|
||||
company: '青岛机场运输公司',
|
||||
contact: '钱七',
|
||||
phonenumber: '12345678924',
|
||||
creator: '王管理',
|
||||
createTime: '2024-04-17 10:48:30',
|
||||
carImage: ''
|
||||
}
|
||||
];
|
||||
// 车辆状态统计数据
|
||||
const vehicleStats = ref([
|
||||
{ title: '在线', count: 45, icon: 'el-icon-success', color: '#67c23a', trend: '+30%', trendUp: true },
|
||||
{ title: '任务中', count: 28, icon: 'el-icon-loading', color: '#409eff', trend: '+20%', trendUp: true },
|
||||
{ title: '故障', count: 5, icon: 'el-icon-warning', color: '#f56c6c', trend: '-10%', trendUp: false },
|
||||
{ title: '离线', count: 12, icon: 'el-icon-error', color: '#e6a23c', trend: '-5%', trendUp: false }
|
||||
]);
|
||||
|
||||
// 车辆导入参数
|
||||
const upload = reactive({
|
||||
open: false,
|
||||
title: "",
|
||||
isUploading: false,
|
||||
updateSupport: 0,
|
||||
headers: { Authorization: "Bearer " + getToken() },
|
||||
url: import.meta.env.VITE_APP_BASE_API + "/car/park/importData"
|
||||
// 统计信息计算
|
||||
const statistics = computed(() => {
|
||||
return {
|
||||
online: vehicleList.value.filter(v => v.status === '在线').length,
|
||||
inTask: vehicleList.value.filter(v => v.routeStatus === '任务中').length,
|
||||
error: vehicleList.value.filter(v => v.status === '故障').length,
|
||||
offline: vehicleList.value.filter(v => v.status === '离线').length
|
||||
};
|
||||
});
|
||||
|
||||
// 查询参数
|
||||
const queryParams = ref({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
carNo: undefined,
|
||||
type: undefined,
|
||||
status: undefined
|
||||
});
|
||||
|
||||
const form = ref({});
|
||||
// 模拟车辆监控数据
|
||||
const mockVehicleData = [
|
||||
{
|
||||
id: 1,
|
||||
carId: 'QH001',
|
||||
type: '驱动车',
|
||||
brand: '大众',
|
||||
organization: '青岛机场物流公司',
|
||||
faultCount: 0,
|
||||
routeStatus: '任务中',
|
||||
status: '在线',
|
||||
chargeStatus: '未充电',
|
||||
batteryLevel: '85%',
|
||||
location: '航站楼A区',
|
||||
lastUpdateTime: '2024-04-17 10:48:30',
|
||||
contact: '张三',
|
||||
phonenumber: '12345678920'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
carId: 'QH002',
|
||||
type: '巡检车',
|
||||
brand: '金龙',
|
||||
organization: '青岛机场运输公司',
|
||||
faultCount: 1,
|
||||
routeStatus: '待命中',
|
||||
status: '在线',
|
||||
chargeStatus: '未充电',
|
||||
batteryLevel: '72%',
|
||||
location: '航站楼B区',
|
||||
lastUpdateTime: '2024-04-17 10:48:30',
|
||||
contact: '李四',
|
||||
phonenumber: '12345678921'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
carId: 'QH003',
|
||||
type: '驱动车',
|
||||
brand: '解放',
|
||||
organization: '青岛机场货运部',
|
||||
faultCount: 2,
|
||||
routeStatus: '充电中',
|
||||
status: '故障',
|
||||
chargeStatus: '充电中',
|
||||
batteryLevel: '45%',
|
||||
location: '充电站',
|
||||
lastUpdateTime: '2024-04-17 10:48:30',
|
||||
contact: '王五',
|
||||
phonenumber: '12345678922'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
carId: 'QH004',
|
||||
type: '驱动车',
|
||||
brand: '福特',
|
||||
organization: '青岛机场物流公司',
|
||||
faultCount: 0,
|
||||
routeStatus: '任务中',
|
||||
status: '在线',
|
||||
chargeStatus: '未充电',
|
||||
batteryLevel: '65%',
|
||||
location: '货运区',
|
||||
lastUpdateTime: '2024-04-17 10:48:30',
|
||||
contact: '赵六',
|
||||
phonenumber: '12345678923'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
carId: 'QH005',
|
||||
type: '巡检车',
|
||||
brand: '宇通',
|
||||
organization: '青岛机场运输公司',
|
||||
faultCount: 0,
|
||||
routeStatus: '待命中',
|
||||
status: '离线',
|
||||
chargeStatus: '未充电',
|
||||
batteryLevel: '0%',
|
||||
location: '维修站',
|
||||
lastUpdateTime: '2024-04-17 10:48:30',
|
||||
contact: '钱七',
|
||||
phonenumber: '12345678924'
|
||||
}
|
||||
];
|
||||
|
||||
const rules = ref({
|
||||
carNo: [
|
||||
{ required: true, message: "车牌号不能为空", trigger: "blur" }
|
||||
],
|
||||
carType: [
|
||||
{ required: true, message: "车型不能为空", trigger: "blur" }
|
||||
],
|
||||
company: [
|
||||
{ required: true, message: "所属公司不能为空", trigger: "blur" }
|
||||
],
|
||||
contact: [
|
||||
{ required: true, message: "联系人不能为空", trigger: "blur" }
|
||||
],
|
||||
phonenumber: [
|
||||
{ required: true, message: "联系电话不能为空", trigger: "blur" },
|
||||
{ pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: "请输入正确的手机号码", trigger: "blur" }
|
||||
]
|
||||
});
|
||||
/** 获取作业状态对应的类型 */
|
||||
function getRouteStatusType(status) {
|
||||
switch (status) {
|
||||
case '任务中':
|
||||
return 'success';
|
||||
case '待命中':
|
||||
return '';
|
||||
case '充电中':
|
||||
return 'warning';
|
||||
case '等待中':
|
||||
return 'info';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取车辆状态对应的类型 */
|
||||
function getStatusType(status) {
|
||||
switch (status) {
|
||||
case '在线':
|
||||
return 'success';
|
||||
case '离线':
|
||||
return 'info';
|
||||
case '故障':
|
||||
return 'danger';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询车辆列表 */
|
||||
function getList() {
|
||||
loading.value = true;
|
||||
// 使用模拟数据
|
||||
setTimeout(() => {
|
||||
// 根据查询条件筛选数据
|
||||
let filteredData = [...mockVehicleData];
|
||||
|
||||
if (queryParams.value.carNo) {
|
||||
const keyword = queryParams.value.carNo.toLowerCase();
|
||||
filteredData = filteredData.filter(item =>
|
||||
item.carId.toLowerCase().includes(keyword) ||
|
||||
item.brand.toLowerCase().includes(keyword) ||
|
||||
item.organization.toLowerCase().includes(keyword)
|
||||
);
|
||||
}
|
||||
|
||||
if (queryParams.value.type) {
|
||||
filteredData = filteredData.filter(item => item.type === queryParams.value.type);
|
||||
}
|
||||
|
||||
if (queryParams.value.status) {
|
||||
filteredData = filteredData.filter(item => item.status === queryParams.value.status);
|
||||
}
|
||||
|
||||
loading.value = false;
|
||||
parkList.value = mockParkData;
|
||||
total.value = mockParkData.length;
|
||||
vehicleList.value = filteredData;
|
||||
total.value = filteredData.length;
|
||||
|
||||
// 更新统计数据
|
||||
vehicleStats.value[0].count = mockVehicleData.filter(v => v.status === '在线').length;
|
||||
vehicleStats.value[1].count = mockVehicleData.filter(v => v.routeStatus === '任务中').length;
|
||||
vehicleStats.value[2].count = mockVehicleData.filter(v => v.status === '故障').length;
|
||||
vehicleStats.value[3].count = mockVehicleData.filter(v => v.status === '离线').length;
|
||||
}, 300);
|
||||
}
|
||||
|
||||
@ -361,8 +301,13 @@ function handleQuery() {
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function resetQuery() {
|
||||
proxy.resetForm("queryRef");
|
||||
queryParams.value.pageNum = 1;
|
||||
queryParams.value = {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
carNo: undefined,
|
||||
type: undefined,
|
||||
status: undefined
|
||||
};
|
||||
handleQuery();
|
||||
}
|
||||
|
||||
@ -373,82 +318,9 @@ function handleSelectionChange(selection) {
|
||||
multiple.value = !selection.length;
|
||||
}
|
||||
|
||||
/** 删除按钮操作 */
|
||||
function handleDelete(row) {
|
||||
const parkIds = row.id || ids.value;
|
||||
proxy.$modal.confirm('确定删除此车辆吗?', '删除').then(function() {
|
||||
// 模拟删除操作
|
||||
getList();
|
||||
proxy.$modal.msgSuccess("删除成功");
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
/** 新增按钮操作 */
|
||||
function handleAdd() {
|
||||
reset();
|
||||
title.value = "添加车辆";
|
||||
open.value = true;
|
||||
}
|
||||
|
||||
/** 修改按钮操作 */
|
||||
function handleUpdate(row) {
|
||||
reset();
|
||||
const parkId = row.id || ids.value[0];
|
||||
// 使用模拟数据
|
||||
const parkInfo = mockParkData.find(d => d.id === parkId);
|
||||
if (parkInfo) {
|
||||
form.value = {...parkInfo};
|
||||
open.value = true;
|
||||
title.value = "修改车辆";
|
||||
}
|
||||
}
|
||||
|
||||
/** 重置表单 */
|
||||
function reset() {
|
||||
form.value = {
|
||||
id: undefined,
|
||||
carNo: undefined,
|
||||
carType: undefined,
|
||||
carBrand: undefined,
|
||||
company: undefined,
|
||||
contact: undefined,
|
||||
phonenumber: undefined,
|
||||
carImage: '',
|
||||
status: "0"
|
||||
};
|
||||
proxy.resetForm("parkRef");
|
||||
}
|
||||
|
||||
/** 提交表单 */
|
||||
function submitForm() {
|
||||
proxy.$refs["parkRef"].validate(valid => {
|
||||
if (valid) {
|
||||
if (form.value.id != undefined) {
|
||||
// 模拟修改操作
|
||||
const index = mockParkData.findIndex(item => item.id === form.value.id);
|
||||
if (index !== -1) {
|
||||
mockParkData[index] = {...form.value};
|
||||
}
|
||||
proxy.$modal.msgSuccess("修改成功");
|
||||
open.value = false;
|
||||
getList();
|
||||
} else {
|
||||
// 模拟新增操作
|
||||
form.value.id = mockParkData.length + 1;
|
||||
form.value.createTime = new Date().toISOString().replace('T', ' ').substring(0, 19);
|
||||
mockParkData.push({...form.value});
|
||||
proxy.$modal.msgSuccess("新增成功");
|
||||
open.value = false;
|
||||
getList();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** 取消按钮 */
|
||||
function cancel() {
|
||||
open.value = false;
|
||||
reset();
|
||||
/** 查看详情按钮操作 */
|
||||
function handleView(row) {
|
||||
proxy.$modal.msgSuccess("查看车辆详情功能已模拟");
|
||||
}
|
||||
|
||||
/** 导出按钮操作 */
|
||||
@ -456,72 +328,6 @@ function handleExport() {
|
||||
proxy.$modal.msgSuccess("导出功能已模拟");
|
||||
}
|
||||
|
||||
/** 导入按钮操作 */
|
||||
function handleImport() {
|
||||
upload.title = "车辆导入";
|
||||
upload.open = true;
|
||||
}
|
||||
|
||||
/** 导入对话框关闭时清除数据 */
|
||||
function handleFileUploadProgress(event, file, fileList) {
|
||||
upload.isUploading = true;
|
||||
}
|
||||
|
||||
/** 导入成功处理 */
|
||||
function handleFileSuccess(response, file, fileList) {
|
||||
upload.open = false;
|
||||
upload.isUploading = false;
|
||||
proxy.$refs["uploadRef"].clearFiles();
|
||||
|
||||
// 模拟导入成功
|
||||
proxy.$modal.msgSuccess("导入成功");
|
||||
mockParkData.push({
|
||||
id: mockParkData.length + 1,
|
||||
carNo: 'QH006',
|
||||
carType: '物流车',
|
||||
status: '0',
|
||||
company: '物流管理公司',
|
||||
contact: '导入用户',
|
||||
phonenumber: '12345678920',
|
||||
createTime: new Date().toISOString().replace('T', ' ').substring(0, 19)
|
||||
});
|
||||
getList();
|
||||
}
|
||||
|
||||
/** 下载导入模板 */
|
||||
function importTemplate() {
|
||||
proxy.$modal.msgSuccess("下载模板功能已模拟");
|
||||
}
|
||||
|
||||
/** 提交上传文件 */
|
||||
function submitFileForm() {
|
||||
proxy.$refs["uploadRef"].submit();
|
||||
}
|
||||
|
||||
// 添加图片上传相关方法
|
||||
const fileInput = ref(null);
|
||||
|
||||
function handleClickUpload() {
|
||||
fileInput.value.click();
|
||||
}
|
||||
|
||||
function handleFileChange(e) {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
form.value.carImage = e.target.result;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
// 清除input的value,否则选择相同文件时不会触发change事件
|
||||
e.target.value = '';
|
||||
}
|
||||
|
||||
function handleRemoveImg() {
|
||||
form.value.carImage = '';
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getList();
|
||||
});
|
||||
@ -545,6 +351,7 @@ onMounted(() => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
margin-top: 20px;
|
||||
|
||||
.search-input {
|
||||
width: 250px;
|
||||
@ -745,218 +552,63 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
// 弹窗样式优化
|
||||
:deep(.el-dialog) {
|
||||
.el-form-item__label {
|
||||
color: #96A0B5 !important;
|
||||
}
|
||||
.el-radio__label {
|
||||
color: #C8C9CC !important;
|
||||
}
|
||||
/* 全局表格样式覆盖,防止闪白 */
|
||||
:deep(.el-table) {
|
||||
--el-table-bg-color: #292c38 !important;
|
||||
--el-table-tr-bg-color: #292c38 !important;
|
||||
--el-table-border-color: #4C4F5F !important;
|
||||
--el-table-header-bg-color: #343744 !important;
|
||||
--el-table-row-hover-bg-color: #2B3B5A !important;
|
||||
|
||||
.el-radio.is-checked .el-radio__label {
|
||||
color: #409EFF !important;
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// 导入弹窗样式
|
||||
.import-dialog {
|
||||
:deep(.el-dialog__header) {
|
||||
margin-right: 0;
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid #4C4F5F;
|
||||
}
|
||||
|
||||
:deep(.el-dialog__body) {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
:deep(.el-dialog__footer) {
|
||||
padding: 20px;
|
||||
border-top: 1px solid #4C4F5F;
|
||||
}
|
||||
|
||||
.import-title {
|
||||
font-size: 14px;
|
||||
color: #96A0B5;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.import-upload {
|
||||
:deep(.el-upload-dragger) {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
background-color: #292C38;
|
||||
border: 1px dashed #4C4F5F;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
border-color: #347AE2;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
color: #96A0B5;
|
||||
|
||||
.upload-icon {
|
||||
font-size: 40px;
|
||||
color: #96A0B5;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.upload-text {
|
||||
font-size: 14px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.upload-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #347AE2;
|
||||
cursor: pointer;
|
||||
|
||||
.upload-icon-small {
|
||||
font-size: 16px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.click-upload {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.import-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 20px;
|
||||
|
||||
.footer-left {
|
||||
:deep(.el-checkbox__label) {
|
||||
color: #96A0B5;
|
||||
}
|
||||
}
|
||||
|
||||
.footer-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
.file-format {
|
||||
color: #96A0B5;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.download-link {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
// 媒体查询,处理响应式布局
|
||||
@media (max-width: 1200px) {
|
||||
.search-area {
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 头像上传样式 */
|
||||
.avatar-uploader-box {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.avatar-uploader {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.avatar-preview {
|
||||
position: relative;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 0;
|
||||
overflow: visible; /* 修改为visible,使计数器可以溢出边界 */
|
||||
|
||||
.avatar {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 0;
|
||||
border: 1px solid #343744;
|
||||
}
|
||||
|
||||
.avatar-count {
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
right: -30px;
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.avatar-replace {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
opacity: 1;
|
||||
z-index: 2;
|
||||
|
||||
.el-icon {
|
||||
font-size: 14px;
|
||||
color: #ffffff;
|
||||
.search-input {
|
||||
width: 100%;
|
||||
max-width: 250px;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.search-select {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.search-buttons {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
margin-left: 0;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-upload-placeholder {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border: 1px dashed #4C4F5F;
|
||||
border-radius: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
position: relative;
|
||||
background-color: transparent;
|
||||
overflow: visible; /* 修改为visible,使计数器可以溢出边界 */
|
||||
|
||||
&:hover {
|
||||
border-color: #409EFF;
|
||||
}
|
||||
|
||||
.avatar-uploader-icon {
|
||||
font-size: 28px;
|
||||
color: #8c939d;
|
||||
}
|
||||
|
||||
.avatar-count {
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
right: -30px;
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
z-index: 1;
|
||||
@media (max-width: 768px) {
|
||||
.search-area {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
.search-input,
|
||||
.search-select {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.search-buttons,
|
||||
.action-buttons {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
@ -1,7 +1,54 @@
|
||||
<template>
|
||||
<p>平台概览</p>
|
||||
<div class="platform-overview">
|
||||
<OpenLayersMap />
|
||||
<CarAlarm />
|
||||
<!-- 右侧展开/收起按钮 -->
|
||||
<img
|
||||
class="eventlist-toggle-btn"
|
||||
:src="showEventList ? btn_left : btn_right"
|
||||
:alt="showEventList ? '收起车辆列表' : '展开车辆列表'"
|
||||
@click="toggleEventList"
|
||||
/>
|
||||
<!-- 车辆列表 -->
|
||||
<Eventlist v-if="showEventList" />
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import OpenLayersMap from '../../components/map/OpenLayersMap.vue';
|
||||
import CarAlarm from '../../components/map/info/carClarm.vue';
|
||||
import Eventlist from '../../components/map/info/eventlist.vue';
|
||||
import btn_left from '../../assets/images/btn_left.png';
|
||||
import btn_right from '../../assets/images/btn_right.png';
|
||||
|
||||
const showEventList = ref(false);
|
||||
|
||||
function toggleEventList() {
|
||||
showEventList.value = !showEventList.value;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.platform-overview {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* 右侧展开/收起按钮样式 */
|
||||
.eventlist-toggle-btn {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 0;
|
||||
z-index: 2001;
|
||||
width: 40px;
|
||||
height: 80px;
|
||||
transform: translateY(-50%);
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
@ -131,7 +131,7 @@
|
||||
|
||||
<!-- 添加或修改参数配置对话框 -->
|
||||
<el-dialog :title="title" v-model="open" width="500px" append-to-body>
|
||||
<el-form ref="configRef" :model="form" :rules="rules" >
|
||||
<el-form ref="configRef" :model="form" :rules="rules" label-width="80px">
|
||||
<el-form-item label="参数名称" prop="configName">
|
||||
<el-input v-model="form.configName" placeholder="请输入参数名称" />
|
||||
</el-form-item>
|
||||
|
||||
@ -126,7 +126,7 @@
|
||||
|
||||
<!-- 添加或修改参数配置对话框 -->
|
||||
<el-dialog :title="title" v-model="open" width="500px" append-to-body>
|
||||
<el-form ref="dataRef" :model="form" :rules="rules" >
|
||||
<el-form ref="dataRef" :model="form" :rules="rules" label-width="80px">
|
||||
<el-form-item label="字典类型">
|
||||
<el-input v-model="form.dictType" :disabled="true" />
|
||||
</el-form-item>
|
||||
|
||||
@ -141,7 +141,7 @@
|
||||
|
||||
<!-- 添加或修改参数配置对话框 -->
|
||||
<el-dialog :title="title" v-model="open" width="500px" append-to-body>
|
||||
<el-form ref="dictRef" :model="form" :rules="rules" >
|
||||
<el-form ref="dictRef" :model="form" :rules="rules" label-width="80px">
|
||||
<el-form-item label="字典名称" prop="dictName">
|
||||
<el-input v-model="form.dictName" placeholder="请输入字典名称" />
|
||||
</el-form-item>
|
||||
|
||||
@ -111,7 +111,7 @@
|
||||
|
||||
<!-- 添加或修改公告对话框 -->
|
||||
<el-dialog :title="title" v-model="open" width="780px" append-to-body>
|
||||
<el-form ref="noticeRef" :model="form" :rules="rules" >
|
||||
<el-form ref="noticeRef" :model="form" :rules="rules" label-width="80px">
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="公告标题" prop="noticeTitle">
|
||||
|
||||
@ -111,7 +111,7 @@
|
||||
|
||||
<!-- 添加或修改岗位对话框 -->
|
||||
<el-dialog :title="title" v-model="open" width="500px" append-to-body>
|
||||
<el-form ref="postRef" :model="form" :rules="rules" >
|
||||
<el-form ref="postRef" :model="form" :rules="rules" label-width="80px">
|
||||
<el-form-item label="岗位名称" prop="postName">
|
||||
<el-input v-model="form.postName" placeholder="请输入岗位名称" />
|
||||
</el-form-item>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<h4 class="form-header h4">基本信息</h4>
|
||||
<el-form :model="form" >
|
||||
<el-form :model="form" label-width="80px">
|
||||
<el-row>
|
||||
<el-col :span="8" :offset="2">
|
||||
<el-form-item label="用户昵称" prop="nickName">
|
||||
|
||||