Merge branch 'main' of http://10.0.0.99:4000/Ren/airport-qingdao-vue3
14
README.md
@ -111,3 +111,17 @@ getStyle(type, item, status) {
|
||||
|
||||
|
||||
当前项目用于青岛机场正式版
|
||||
|
||||
|
||||
|
||||
/** 获取用户列表 */
|
||||
function getUserOptions() {
|
||||
|
||||
getRoleUsers(3).then(res => {
|
||||
userOptions.value = res.rows;
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
使用SockJS + STOMP协议
|
||||
滑出蓝色 滑入黄色
|
||||
@ -15,11 +15,10 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "2.0.10",
|
||||
"@stomp/stompjs": "^7.1.1",
|
||||
"@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",
|
||||
@ -28,7 +27,10 @@
|
||||
"jsencrypt": "3.3.1",
|
||||
"leaflet-minimap": "^3.6.1",
|
||||
"nprogress": "0.2.0",
|
||||
"ol": "6.15.1",
|
||||
"pinia": "2.0.22",
|
||||
"proj4": "^2.17.0",
|
||||
"sockjs-client": "^1.6.1",
|
||||
"vue": "3.2.45",
|
||||
"vue-cropper": "1.0.3",
|
||||
"vue-router": "4.1.4"
|
||||
|
||||
44
src/api/system/driver_info.js
Normal file
@ -0,0 +1,44 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 查询驾驶员信息列表
|
||||
export function listDriver_info(query) {
|
||||
return request({
|
||||
url: '/system/driver_info/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 查询驾驶员信息详细
|
||||
export function getDriver_info(userId) {
|
||||
return request({
|
||||
url: '/system/driver_info/' + userId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 新增驾驶员信息
|
||||
export function addDriver_info(data) {
|
||||
return request({
|
||||
url: '/system/driver_info',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 修改驾驶员信息
|
||||
export function updateDriver_info(data) {
|
||||
return request({
|
||||
url: '/system/driver_info',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除驾驶员信息
|
||||
export function delDriver_info(userId) {
|
||||
return request({
|
||||
url: '/system/driver_info/' + userId,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
@ -16,6 +16,13 @@ export function getRole(roleId) {
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
// 查询角色
|
||||
export function getRoleUsers(roleId) {
|
||||
return request({
|
||||
url: '/system/role/users/' + roleId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 新增角色
|
||||
export function addRole(data) {
|
||||
|
||||
44
src/api/system/vehicle_info.js
Normal file
@ -0,0 +1,44 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 查询车辆信息列表
|
||||
export function listVehicle_info(query) {
|
||||
return request({
|
||||
url: '/system/vehicle_info/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 查询车辆信息详细
|
||||
export function getVehicle_info(vehicleId) {
|
||||
return request({
|
||||
url: '/system/vehicle_info/' + vehicleId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 新增车辆信息
|
||||
export function addVehicle_info(data) {
|
||||
return request({
|
||||
url: '/system/vehicle_info',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 修改车辆信息
|
||||
export function updateVehicle_info(data) {
|
||||
return request({
|
||||
url: '/system/vehicle_info',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除车辆信息
|
||||
export function delVehicle_info(vehicleId) {
|
||||
return request({
|
||||
url: '/system/vehicle_info/' + vehicleId,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
44
src/api/system/vehicle_type.js
Normal file
@ -0,0 +1,44 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 查询车辆类型列表
|
||||
export function listVehicle_type(query) {
|
||||
return request({
|
||||
url: '/system/vehicle_type/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 查询车辆类型详细
|
||||
export function getVehicle_type(typeId) {
|
||||
return request({
|
||||
url: '/system/vehicle_type/' + typeId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 新增车辆类型
|
||||
export function addVehicle_type(data) {
|
||||
return request({
|
||||
url: '/system/vehicle_type',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 修改车辆类型
|
||||
export function updateVehicle_type(data) {
|
||||
return request({
|
||||
url: '/system/vehicle_type',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除车辆类型
|
||||
export function delVehicle_type(typeId) {
|
||||
return request({
|
||||
url: '/system/vehicle_type/' + typeId,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
BIN
src/assets/images/1btn.png
Normal file
|
After Width: | Height: | Size: 206 B |
BIN
src/assets/images/2btn.png
Normal file
|
After Width: | Height: | Size: 211 B |
BIN
src/assets/images/6btn.png
Normal file
|
After Width: | Height: | Size: 339 B |
BIN
src/assets/images/Aircraft1.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
src/assets/images/airport_bg.png
Normal file
|
After Width: | Height: | Size: 609 B |
BIN
src/assets/images/airport_out.png
Normal file
|
After Width: | Height: | Size: 612 B |
BIN
src/assets/images/alarm_bg.png
Normal file
|
After Width: | Height: | Size: 597 B |
BIN
src/assets/images/alarm_icon.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
src/assets/images/alarm_report.png
Normal file
|
After Width: | Height: | Size: 104 KiB |
BIN
src/assets/images/battery.png
Normal file
|
After Width: | Height: | Size: 332 B |
BIN
src/assets/images/battery_health.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
src/assets/images/battery_sum.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
src/assets/images/choice.png
Normal file
|
After Width: | Height: | Size: 658 B |
BIN
src/assets/images/clarm_conflict.png
Normal file
|
After Width: | Height: | Size: 693 B |
BIN
src/assets/images/clarm_over.png
Normal file
|
After Width: | Height: | Size: 951 B |
BIN
src/assets/images/del.png
Normal file
|
After Width: | Height: | Size: 491 B |
BIN
src/assets/images/label_bg.png
Normal file
|
After Width: | Height: | Size: 604 B |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.1 KiB |
BIN
src/assets/images/left_arrow.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
src/assets/images/left_arrow_active.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
src/assets/images/monitor_tab1.png
Normal file
|
After Width: | Height: | Size: 454 B |
BIN
src/assets/images/monitor_tab2.png
Normal file
|
After Width: | Height: | Size: 389 B |
BIN
src/assets/images/right_arrow.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
src/assets/images/right_arrow_active.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
src/assets/images/tab1.png
Normal file
|
After Width: | Height: | Size: 270 B |
BIN
src/assets/images/tab2.png
Normal file
|
After Width: | Height: | Size: 377 B |
BIN
src/assets/images/tab3.png
Normal file
|
After Width: | Height: | Size: 446 B |
BIN
src/assets/images/tab4.png
Normal file
|
After Width: | Height: | Size: 363 B |
BIN
src/assets/images/tab5.png
Normal file
|
After Width: | Height: | Size: 321 B |
BIN
src/assets/images/warn_icon.png
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
BIN
src/assets/images/warn_report.png
Normal file
|
After Width: | Height: | Size: 95 KiB |
BIN
src/assets/images/warning_bg.png
Normal file
|
After Width: | Height: | Size: 571 B |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 1.1 KiB |
BIN
src/assets/images/znzBg.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
@ -49,7 +49,7 @@
|
||||
background: #343744 !important;
|
||||
border: none !important;
|
||||
border-radius: 8px !important;
|
||||
height: 36px !important;
|
||||
height: 36px;
|
||||
font-size: 15px;
|
||||
color: #96A0B5 !important;
|
||||
box-shadow: none !important;
|
||||
|
||||
@ -42,6 +42,7 @@ $--color-warning: #E6A23C;
|
||||
$--color-danger: #F56C6C;
|
||||
$--color-info: #909399;
|
||||
|
||||
|
||||
$base-sidebar-width: 280px;
|
||||
|
||||
// the :export directive is the magic sauce for webpack
|
||||
|
||||
@ -289,8 +289,8 @@ function handlePagination({ page, limit }) {
|
||||
|
||||
.cards-grid {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
@ -300,7 +300,7 @@ function handlePagination({ page, limit }) {
|
||||
color: #ffffff;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
|
||||
width: 100%;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
position: relative;
|
||||
|
||||
|
||||
@ -102,6 +102,7 @@ const statistics = computed(() => {
|
||||
<style lang="scss" scoped>
|
||||
.stats-container {
|
||||
width: 100%;
|
||||
|
||||
}
|
||||
|
||||
.stat-cards {
|
||||
|
||||
@ -132,7 +132,6 @@ const blocks = [
|
||||
.battery-overview-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
justify-content: space-between;
|
||||
|
||||
}
|
||||
@ -159,6 +158,7 @@ const blocks = [
|
||||
border-radius: 4px;
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
|
||||
padding: 2px 8px;
|
||||
margin-right: 10px;
|
||||
|
||||
@ -168,6 +168,7 @@ const blocks = [
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
padding-bottom: 2px;
|
||||
|
||||
border-bottom: 2px solid rgba(109, 184, 255, 0.5);
|
||||
}
|
||||
.block-list {
|
||||
@ -182,11 +183,13 @@ const blocks = [
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: no-wrap;
|
||||
|
||||
}
|
||||
.item-no {
|
||||
color: #BDC1C6;
|
||||
font-size: 13px;
|
||||
min-width: 2em;
|
||||
|
||||
display: inline-block;
|
||||
|
||||
|
||||
@ -200,8 +203,8 @@ const blocks = [
|
||||
color: #fff;
|
||||
}
|
||||
.right-panel {
|
||||
/* flex: 1; */
|
||||
/* background: #23263a; */
|
||||
flex: 1;
|
||||
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@ -1,167 +1,381 @@
|
||||
<!-- 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 class="stats-container">
|
||||
<!-- 左侧统计卡片 -->
|
||||
<div class="stats-cards">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">
|
||||
<img src="@/assets/images/battery_sum.png" alt="充电次数" />
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-title">充电总次数</div>
|
||||
<div class="stat-value">{{ stats.totalCount }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">
|
||||
<img src="@/assets/images/battery_health.png" alt="电池健康状态" />
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-title">电池健康状态</div>
|
||||
<div class="stat-value">{{ stats.health }}%</div>
|
||||
</div>
|
||||
</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 class="charts-area">
|
||||
<!-- 充电时长分析图表 -->
|
||||
<charging-duration-chart />
|
||||
|
||||
<!-- 充放电量对比图表 -->
|
||||
<charging-comparison-chart />
|
||||
</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 class="date-range">
|
||||
<span class="date-label">充电记录</span>
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
size="default"
|
||||
class="date-picker"
|
||||
/>
|
||||
<el-button type="primary" class="filter-btn" @click="doFilter">搜索</el-button>
|
||||
<el-button class="reset-btn" @click="resetFilter">重置</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表格区域 -->
|
||||
<el-table
|
||||
:data="pagedData"
|
||||
style="width: 100%"
|
||||
:header-cell-style="{ background: '#23263a', color: '#fff' }"
|
||||
:header-cell-style="{ backgroundColor: '#343744', color: '#96A0B5', fontWeight: 'normal' }"
|
||||
class="custom-table"
|
||||
>
|
||||
<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-column prop="index" label="序号" width="60" align="center" />
|
||||
<el-table-column prop="carId" label="车辆编号" min-width="180" />
|
||||
<el-table-column prop="vin" label="车辆识别码" min-width="120" />
|
||||
<el-table-column prop="chargeStatus" label="充电状态" width="100" />
|
||||
<el-table-column prop="startTime" label="开始时间" min-width="160" />
|
||||
<el-table-column prop="endTime" label="结束时间" min-width="160" />
|
||||
<el-table-column prop="chargePile" label="充电桩" width="100" />
|
||||
<el-table-column prop="operator" label="充电操作员" width="120" />
|
||||
</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>
|
||||
|
||||
<!-- 分页组件 -->
|
||||
<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';
|
||||
import ChargingDurationChart from './charts/ChargingDurationChart.vue';
|
||||
import ChargingComparisonChart from './charts/ChargingComparisonChart.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",
|
||||
carId: "DONGLIHANUN5695",
|
||||
vin: "ANA45332156",
|
||||
chargeStatus: "停车充电",
|
||||
startTime: "2024-08-17 15:48:30",
|
||||
endTime: "2024-08-17 15:48:30",
|
||||
endTime: "2024-08-17 17:48:30",
|
||||
chargePile: "T3点位",
|
||||
operator: "张三",
|
||||
},
|
||||
{
|
||||
index: 2,
|
||||
carId: "DONGJIHUANUN5695",
|
||||
vin: "ANA53532156",
|
||||
carId: "DONGLIHANUN5695",
|
||||
vin: "ANA45332156",
|
||||
chargeStatus: "停车充电",
|
||||
startTime: "2024-08-17 15:48:30",
|
||||
endTime: "2024-08-17 15:48:30",
|
||||
endTime: "2024-08-17 17:48:30",
|
||||
chargePile: "T3点位",
|
||||
operator: "张三",
|
||||
},
|
||||
{
|
||||
index: 3,
|
||||
carId: "DONGJIHUANUN5695",
|
||||
vin: "ANA53532156",
|
||||
carId: "DONGLIHANUN5695",
|
||||
vin: "ANA45332156",
|
||||
chargeStatus: "停车充电",
|
||||
startTime: "2024-08-17 15:48:30",
|
||||
endTime: "2024-08-17 15:48:30",
|
||||
endTime: "2024-08-17 17: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;
|
||||
}
|
||||
|
||||
// 重置筛选
|
||||
function resetFilter() {
|
||||
dateRange.value = null;
|
||||
page.value = 1;
|
||||
}
|
||||
|
||||
// 处理分页
|
||||
function handlePagination({ page: newPage, limit: newLimit }) {
|
||||
page.value = newPage;
|
||||
pageSize.value = newLimit;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
<style scoped lang="scss">
|
||||
.charging-stats {
|
||||
background: #23263a;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
padding: 0;
|
||||
color: #fff;
|
||||
}
|
||||
.top-row {
|
||||
|
||||
/* 顶部统计和图表容器 */
|
||||
.stats-container {
|
||||
display: flex;
|
||||
gap: 18px;
|
||||
margin-bottom: 18px;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
}
|
||||
|
||||
/* 左侧统计卡片 */
|
||||
.stats-cards {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: #1e2233;
|
||||
background: #343744;
|
||||
border-radius: 8px;
|
||||
padding: 18px 24px;
|
||||
min-width: 120px;
|
||||
text-align: center;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
.stat-title {
|
||||
color: #4ea1ff;
|
||||
font-size: 15px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.stat-value {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.chart-area {
|
||||
background: #1e2233;
|
||||
|
||||
.stat-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
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;
|
||||
background: #343744;
|
||||
color: #4EA1FF;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.filter-bar {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.pagination-bar {
|
||||
margin-top: 12px;
|
||||
|
||||
.circle-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: #343744;
|
||||
color: #4EA1FF;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.stat-title {
|
||||
color: #96A0B5;
|
||||
font-size: 14px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 右侧图表区域 */
|
||||
.charts-area {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
/* 筛选区域 */
|
||||
.filter-bar {
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.date-range {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.date-label {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.date-picker {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.filter-btn {
|
||||
background: #347AE2;
|
||||
border-color: #347AE2;
|
||||
}
|
||||
|
||||
.reset-btn {
|
||||
background: #343744;
|
||||
border-color: #343744;
|
||||
color: #96A0B5;
|
||||
}
|
||||
|
||||
/* 表格样式 */
|
||||
.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-date-editor) {
|
||||
--el-datepicker-border-color: #343744;
|
||||
--el-datepicker-inner-border-color: #343744;
|
||||
--el-datepicker-inrange-bg-color: #343744;
|
||||
|
||||
.el-input__wrapper {
|
||||
background-color: #343744 !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.el-input__inner {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.el-range-separator {
|
||||
color: #96A0B5 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
<!-- TrackPlayback.vue(轨迹回放) -->
|
||||
<!-- TrackPlayback.vue(轨迹回放页面UI还原) -->
|
||||
<template>
|
||||
<div class="track-playback">
|
||||
<div class="track-playback-content">
|
||||
<!-- 左侧搜索+列表 -->
|
||||
<div class="left-list">
|
||||
|
||||
<el-input
|
||||
v-model="search"
|
||||
placeholder="请输入任务号/车辆名"
|
||||
clearable
|
||||
class="search-input"
|
||||
placeholder="请输入任务号/车辆名"
|
||||
:suffix-icon="Search"
|
||||
/>
|
||||
<el-scrollbar class="task-list">
|
||||
<div
|
||||
@ -16,21 +18,102 @@
|
||||
: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 v-if="item.id === activeId" class="corner-triangle">
|
||||
|
||||
</div>
|
||||
<!-- 第一行:蓝色圆点+编号+任务名 -->
|
||||
<div class="task-row1">
|
||||
<span class="dot"></span>
|
||||
<span class="task-no">{{ item.no }}</span>
|
||||
<span class="task-name">{{ item.name }}</span>
|
||||
</div>
|
||||
<!-- 第二行:时间段 -->
|
||||
<div class="task-row2">{{ item.time }}</div>
|
||||
<!-- 第三行:起点——>终点 -->
|
||||
<div class="task-row3">
|
||||
<span class="point start">起点</span>
|
||||
<span class="label">{{ item.start }}</span>
|
||||
<span class="arrow">——></span>
|
||||
<span class="point end">终点</span>
|
||||
<span class="label">{{ item.end }}</span>
|
||||
</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 class="map-img-placeholder"></div>
|
||||
<!-- 轨迹详情卡片(左上角) -->
|
||||
<div class="track-detail-panel">
|
||||
<!-- 第一行 -->
|
||||
<div class="panel-header">
|
||||
<span class="dot"></span>
|
||||
<span class="panel-title">轨迹详情</span>
|
||||
<el-button size="small" type="primary" class="replay-btn">回放</el-button>
|
||||
</div>
|
||||
<!-- 第二行 -->
|
||||
<div class="panel-info">
|
||||
<div class="info-row">
|
||||
<span class="carno">QN001</span>
|
||||
<span class="info-item">最大时速 <b>91km/h</b></span>
|
||||
<span class="info-item">平均时速 <b>28km/h</b></span>
|
||||
<span class="info-item">总里程 <b>63.3km</b></span>
|
||||
<span class="info-item">耗时 <b>20min</b></span>
|
||||
<span class="info-item warn">冲突告警 <b>1</b></span>
|
||||
<span class="info-item prewarn">冲突预警 <b>1</b></span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 第三行:进度条 -->
|
||||
<div class="panel-progress">
|
||||
<div class="progress-bar-wrap"
|
||||
@mousemove="handleProgressHover"
|
||||
@mouseleave="showTooltip = false"
|
||||
@click="handleProgressClick">
|
||||
<div class="progress-bar-bg"></div>
|
||||
<div class="progress-bar-fg" :style="{width: progress+'%'}"></div>
|
||||
<!-- 进度条拖动点 -->
|
||||
<div class="progress-thumb" :style="{left: progress+'%'}" @mousedown="startDrag"></div>
|
||||
<!-- 预警/告警红旗 -->
|
||||
<div
|
||||
v-for="flag in flags"
|
||||
:key="flag.time"
|
||||
class="progress-flag"
|
||||
:style="{left: flag.percent+'%'}"
|
||||
:title="flag.label"
|
||||
>
|
||||
<svg width="14" height="18" viewBox="0 0 14 18">
|
||||
<polygon points="2,2 12,5 2,8" fill="#E34D59"/>
|
||||
<rect x="1" y="2" width="2" height="14" fill="#E34D59"/>
|
||||
</svg>
|
||||
</div>
|
||||
<!-- 鼠标悬浮提示 -->
|
||||
<div v-if="showTooltip" class="progress-tooltip" :style="{left: tooltipLeft+'px'}">
|
||||
{{ tooltipTime }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress-bottom">
|
||||
<span class="start-time">2024-09-10 12:00:00</span>
|
||||
<div class="speed-select">
|
||||
<el-dropdown @command="setSpeed">
|
||||
<span class="el-dropdown-link">
|
||||
{{ speed }}x <i class="el-icon-arrow-down"></i>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="1">1x</el-dropdown-item>
|
||||
<el-dropdown-item command="2">2x</el-dropdown-item>
|
||||
<el-dropdown-item command="4">4x</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
<span class="end-time">2024-09-10 12:20:00</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -38,20 +121,38 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from "vue";
|
||||
const search = ref("");
|
||||
const activeId = ref(1);
|
||||
import { Search } from "@element-plus/icons-vue";
|
||||
|
||||
// 车辆信息模拟数据
|
||||
const carInfo = {
|
||||
img: "https://cdn.jsdelivr.net/gh/duogongneng/testcdn/car-demo.jpg",
|
||||
no: "QN001",
|
||||
status: "在线",
|
||||
statusClass: "online",
|
||||
type: "货运车",
|
||||
driver: "张三",
|
||||
phone: "15689742356",
|
||||
};
|
||||
|
||||
// 任务模拟数据
|
||||
const tasks = [
|
||||
{
|
||||
id: 1,
|
||||
name: "东园区巡逻",
|
||||
time: "2024年8月16日 18:12:09",
|
||||
no: "001",
|
||||
name: "东园区驱鸟",
|
||||
time: "2024年8月16日 18:12:06--2024年8月18日 18:12:09",
|
||||
start: "航站楼01西门",
|
||||
end: "航站楼02东门",
|
||||
status: "已完成",
|
||||
speed: "28km/h",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "西园区巡逻",
|
||||
time: "2024年8月16日 18:12:09",
|
||||
no: "002",
|
||||
name: "西园区驱鸟",
|
||||
time: "2024年8月16日 18:12:06--2024年8月18日 18:12:09",
|
||||
start: "航站楼03西门",
|
||||
end: "航站楼04东门",
|
||||
status: "已完成",
|
||||
speed: "27km/h",
|
||||
},
|
||||
@ -70,29 +171,97 @@ const tasks = [
|
||||
speed: "26km/h",
|
||||
},
|
||||
];
|
||||
|
||||
const search = ref("");
|
||||
const activeId = ref(tasks[0].id);
|
||||
|
||||
const filteredTasks = computed(() => {
|
||||
if (!search.value) return tasks;
|
||||
return tasks.filter(
|
||||
(t) =>
|
||||
t.name.includes(search.value) || t.id.toString().includes(search.value)
|
||||
t.name.includes(search.value) ||
|
||||
t.no.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;
|
||||
}
|
||||
|
||||
const progress = ref(30); // 进度百分比
|
||||
const speed = ref(1);
|
||||
const showTooltip = ref(false);
|
||||
const tooltipTime = ref("");
|
||||
const tooltipLeft = ref(0);
|
||||
|
||||
// 预警/告警红旗模拟数据
|
||||
const flags = [
|
||||
{ percent: 20, label: "告警 12:04:00" },
|
||||
{ percent: 60, label: "预警 12:12:00" }
|
||||
];
|
||||
|
||||
// 拖动进度条
|
||||
let dragging = false;
|
||||
function startDrag(e) {
|
||||
dragging = true;
|
||||
document.addEventListener("mousemove", onDrag);
|
||||
document.addEventListener("mouseup", stopDrag);
|
||||
}
|
||||
function onDrag(e) {
|
||||
if (!dragging) return;
|
||||
const bar = document.querySelector(".progress-bar-wrap");
|
||||
const rect = bar.getBoundingClientRect();
|
||||
let percent = ((e.clientX - rect.left) / rect.width) * 100;
|
||||
percent = Math.max(0, Math.min(100, percent));
|
||||
progress.value = percent;
|
||||
}
|
||||
function stopDrag() {
|
||||
dragging = false;
|
||||
document.removeEventListener("mousemove", onDrag);
|
||||
document.removeEventListener("mouseup", stopDrag);
|
||||
}
|
||||
|
||||
// 鼠标悬浮显示时分秒
|
||||
function handleProgressHover(e) {
|
||||
const bar = e.currentTarget;
|
||||
const rect = bar.getBoundingClientRect();
|
||||
let percent = ((e.clientX - rect.left) / rect.width) * 100;
|
||||
percent = Math.max(0, Math.min(100, percent));
|
||||
tooltipLeft.value = e.clientX - rect.left;
|
||||
// 假设总时长20分钟
|
||||
const totalSec = 20 * 60;
|
||||
const sec = Math.round((percent / 100) * totalSec);
|
||||
const mm = String(Math.floor(sec / 60)).padStart(2, "0");
|
||||
const ss = String(sec % 60).padStart(2, "0");
|
||||
tooltipTime.value = `${mm}:${ss}`;
|
||||
showTooltip.value = true;
|
||||
}
|
||||
function handleProgressClick(e) {
|
||||
const bar = e.currentTarget;
|
||||
const rect = bar.getBoundingClientRect();
|
||||
let percent = ((e.clientX - rect.left) / rect.width) * 100;
|
||||
percent = Math.max(0, Math.min(100, percent));
|
||||
progress.value = percent;
|
||||
}
|
||||
function setSpeed(val) {
|
||||
speed.value = val;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.track-playback {
|
||||
<style scoped lang="scss">
|
||||
.track-playback-content {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
flex: 1;
|
||||
}
|
||||
.left-list {
|
||||
width: 260px;
|
||||
background: #23263a;
|
||||
width: 25%;
|
||||
|
||||
border-radius: 8px;
|
||||
padding: 16px 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.search-input {
|
||||
margin-bottom: 10px;
|
||||
@ -101,49 +270,249 @@ function selectTask(item) {
|
||||
max-height: 400px;
|
||||
}
|
||||
.task-item {
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 8px;
|
||||
position: relative;
|
||||
padding: 16px 16px 14px 16px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 14px;
|
||||
cursor: pointer;
|
||||
background: #1e2233;
|
||||
background: #343744;
|
||||
color: #fff;
|
||||
border: 2px solid transparent;
|
||||
transition: border 0.2s, box-shadow 0.2s;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.task-item.active {
|
||||
background: #409eff;
|
||||
color: #fff;
|
||||
border: 2px solid #347ae2;
|
||||
box-shadow: 0 2px 8px rgba(52,122,226,0.08);
|
||||
}
|
||||
.task-title {
|
||||
.corner-triangle {
|
||||
background: url('@/assets/images/choice.png') no-repeat 100% 100%;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: 38px;
|
||||
height: 33px;
|
||||
z-index: 2;
|
||||
}
|
||||
.task-row1 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 4px;
|
||||
.dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #347ae2;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin-right: 2px;
|
||||
}
|
||||
.task-no {
|
||||
color: #347ae2;
|
||||
font-weight: bold;
|
||||
}
|
||||
.task-name {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
.task-time,
|
||||
.task-status {
|
||||
.task-row2 {
|
||||
font-size: 13px;
|
||||
color: #b0b8c9;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.task-row3 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
.label{
|
||||
font-size: 13px;
|
||||
color:#fff;
|
||||
}
|
||||
.point {
|
||||
padding: 0px 5px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
background: transparent;
|
||||
border: 1.5px solid;
|
||||
&.start {
|
||||
border-color: #347ae2;
|
||||
background: rgba(52, 122, 226, 0.20);
|
||||
color: #347ae2;
|
||||
}
|
||||
&.end {
|
||||
border-color: #27ae60;
|
||||
background: rgba(39, 174, 96, 0.20);
|
||||
color: #27ae60;
|
||||
}
|
||||
}
|
||||
.arrow {
|
||||
color: #b0b8c9;
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
.right-map {
|
||||
flex: 1;
|
||||
background: #23263a;
|
||||
border: 1px solid #343744;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.map-header {
|
||||
color: #4ea1ff;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
.map-container {
|
||||
flex: 1;
|
||||
background: #1e2233;
|
||||
|
||||
border-radius: 8px;
|
||||
min-height: 320px;
|
||||
min-height: 400px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.map-placeholder {
|
||||
color: #888;
|
||||
font-size: 18px;
|
||||
.map-img-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
border-radius: 12px;
|
||||
}
|
||||
.track-detail-panel {
|
||||
position: absolute;
|
||||
top: 24px;
|
||||
left: 24px;
|
||||
min-width: 520px;
|
||||
background: rgba(41,44,56,0.8);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,0.18);
|
||||
padding: 18px 28px 18px 20px;
|
||||
z-index: 10;
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
.dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: #347ae2;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
.panel-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #4ea1ff;
|
||||
margin-right: 12px;
|
||||
}
|
||||
.replay-btn {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
.panel-info {
|
||||
margin-bottom: 10px;
|
||||
.info-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 18px;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
.carno {
|
||||
color: #4ea1ff;
|
||||
font-weight: bold;
|
||||
font-size: 15px;
|
||||
}
|
||||
.info-item {
|
||||
color: #b0b8c9;
|
||||
b { color: #fff; font-weight: 500; }
|
||||
&.warn b { color: #e34d59; }
|
||||
&.prewarn b { color: #f7b500; }
|
||||
}
|
||||
}
|
||||
}
|
||||
.panel-progress {
|
||||
margin-top: 8px;
|
||||
.progress-bar-wrap {
|
||||
position: relative;
|
||||
height: 8px;
|
||||
background: transparent;
|
||||
margin-bottom: 8px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
.progress-bar-bg {
|
||||
position: absolute;
|
||||
left: 0; top: 0; right: 0; bottom: 0;
|
||||
background: #343744;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.progress-bar-fg {
|
||||
position: absolute;
|
||||
left: 0; top: 0; bottom: 0;
|
||||
background: linear-gradient(90deg, #347ae2, #4ea1ff);
|
||||
border-radius: 4px;
|
||||
height: 8px;
|
||||
z-index: 1;
|
||||
}
|
||||
.progress-thumb {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: #fff;
|
||||
border: 3px solid #347ae2;
|
||||
border-radius: 50%;
|
||||
z-index: 2;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 8px rgba(52,122,226,0.12);
|
||||
}
|
||||
.progress-flag {
|
||||
position: absolute;
|
||||
top: -14px;
|
||||
z-index: 3;
|
||||
width: 14px;
|
||||
height: 18px;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.progress-tooltip {
|
||||
position: absolute;
|
||||
top: -32px;
|
||||
background: #23263a;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
white-space: nowrap;
|
||||
box-shadow: 0 2px 8px rgba(52,122,226,0.10);
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
}
|
||||
.progress-bottom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 13px;
|
||||
color: #b0b8c9;
|
||||
.speed-select {
|
||||
margin: 0 16px;
|
||||
.el-dropdown-link {
|
||||
color: #4ea1ff;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
.start-time, .end-time {
|
||||
font-size: 13px;
|
||||
color: #b0b8c9;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,95 +1,96 @@
|
||||
<!-- 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 class="video-monitoring-content">
|
||||
<!-- 画面区 -->
|
||||
<div v-if="props.layoutType === '1'" class="one-view">
|
||||
<img v-if="pagedData[0]" :src="pagedData[0].img" class="video-img" />
|
||||
</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 v-else-if="props.layoutType === '2'" class="two-view">
|
||||
<img v-for="item in pagedData.slice(0, 2)" :key="item.id" :src="item.img" class="video-img" />
|
||||
</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 v-else class="six-view">
|
||||
<img v-for="item in pagedData.slice(0, 6)" :key="item.id" :src="item.img" class="video-img" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 使用Pagination组件替换el-pagination -->
|
||||
<pagination
|
||||
v-show="allData.length > 0"
|
||||
:total="allData.length"
|
||||
v-model:page="page"
|
||||
v-model:limit="pageSize"
|
||||
@pagination="handlePagination"
|
||||
/>
|
||||
</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) => ({
|
||||
import { computed, ref } from "vue";
|
||||
import Pagination from '@/components/Pagination/index.vue';
|
||||
|
||||
const props = defineProps({
|
||||
layoutType: { type: String, default: '6' },
|
||||
vehicle: { type: Object, default: () => ({}) }
|
||||
});
|
||||
|
||||
const allData = ref(Array.from({ length: 20 }).map((_, i) => ({
|
||||
id: i + 1,
|
||||
img: `https://picsum.photos/seed/${i + 1}/400/220`,
|
||||
}));
|
||||
})));
|
||||
const page = ref(1);
|
||||
const pageSize = ref(6);
|
||||
const pagedData = computed(() => {
|
||||
const start = (page.value - 1) * pageSize.value;
|
||||
return allData.slice(start, start + pageSize.value);
|
||||
return allData.value.slice(start, start + pageSize.value);
|
||||
});
|
||||
|
||||
function handlePagination({ page: newPage, limit: newLimit }) {
|
||||
page.value = newPage;
|
||||
pageSize.value = newLimit;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.video-monitoring {
|
||||
background: #23263a;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
|
||||
|
||||
color: #fff;
|
||||
}
|
||||
.toolbar {
|
||||
margin-bottom: 12px;
|
||||
.video-monitoring-content{
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.grid-view {
|
||||
.one-view .video-img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
aspect-ratio: 16/9;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
background: #1e2233;
|
||||
}
|
||||
.two-view {
|
||||
display: flex;
|
||||
gap: 18px;
|
||||
}
|
||||
.two-view .video-img {
|
||||
flex: 1;
|
||||
height: auto;
|
||||
aspect-ratio: 16/9;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
background: #1e2233;
|
||||
}
|
||||
.six-view {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 18px;
|
||||
}
|
||||
.video-card {
|
||||
background: #1e2233;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.video-img {
|
||||
.six-view .video-img {
|
||||
width: 100%;
|
||||
height: 180px;
|
||||
height: auto;
|
||||
aspect-ratio: 16/9;
|
||||
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;
|
||||
background: #1e2233;
|
||||
}
|
||||
</style>
|
||||
|
||||
320
src/components/car/detail/charts/ChargingComparisonChart.vue
Normal file
@ -0,0 +1,320 @@
|
||||
<!-- 充放电量对比图表组件 -->
|
||||
<template>
|
||||
<div class="chart-container">
|
||||
<div class="chart-header">
|
||||
<div class="chart-title">充放电量对比</div>
|
||||
</div>
|
||||
<!-- <div class="chart-center-legend">
|
||||
<span class="legend-item">
|
||||
<span class="legend-dot" style="background-color: #347AE2;"></span>
|
||||
<span>充电量</span>
|
||||
</span>
|
||||
<span class="legend-item">
|
||||
<span class="legend-dot" style="background-color: #B8D4FE;"></span>
|
||||
<span>放电量</span>
|
||||
</span>
|
||||
</div> -->
|
||||
<div class="chart-body" ref="chartRef"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, nextTick, onBeforeUnmount } from 'vue';
|
||||
import * as echarts from 'echarts/core';
|
||||
import { LineChart } from 'echarts/charts';
|
||||
import { GridComponent, TooltipComponent, TitleComponent, LegendComponent } from 'echarts/components';
|
||||
import { CanvasRenderer } from 'echarts/renderers';
|
||||
|
||||
// 注册必要的组件
|
||||
echarts.use([LineChart, GridComponent, TooltipComponent, TitleComponent, LegendComponent, CanvasRenderer]);
|
||||
|
||||
const chartRef = ref(null);
|
||||
let chartInstance = null;
|
||||
|
||||
// 模拟数据
|
||||
const chartData = {
|
||||
dates: ['7.2', '7.3', '7.4', '7.5', '7.6', '7.7', '7.8', '7.9', '7.10', '7.11', '7.12'],
|
||||
chargeData: [45, 65, 55, 80, 60, 55, 70, 60, 85, 65, 75],
|
||||
dischargeData: [30, 45, 35, 50, 40, 30, 45, 35, 60, 40, 50]
|
||||
};
|
||||
|
||||
// 初始化图表
|
||||
function initChart() {
|
||||
if (!chartRef.value) return;
|
||||
|
||||
// 创建图表实例
|
||||
chartInstance = echarts.init(chartRef.value);
|
||||
|
||||
// 找出充电量和放电量的最大值索引
|
||||
const chargeMaxIndex = chartData.chargeData.indexOf(Math.max(...chartData.chargeData));
|
||||
const dischargeMaxIndex = chartData.dischargeData.indexOf(Math.max(...chartData.dischargeData));
|
||||
|
||||
// 设置图表配置项
|
||||
const option = {
|
||||
grid: {
|
||||
left: '2%',
|
||||
right: '2%',
|
||||
bottom: '10%',
|
||||
top: '8%',
|
||||
containLabel: true
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
backgroundColor: '#343744',
|
||||
borderColor: '#4C4F5F',
|
||||
textStyle: {
|
||||
color: '#fff'
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: ['充电量', '放电量'],
|
||||
top: '0',
|
||||
itemWidth: 10,
|
||||
itemHeight: 10,
|
||||
itemGap: 10,
|
||||
textStyle: {
|
||||
color: '#96A0B5',
|
||||
fontSize: 12
|
||||
}
|
||||
},
|
||||
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: chartData.dates,
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#4C4F5F'
|
||||
}
|
||||
},
|
||||
axisLabel: {
|
||||
color: '#96A0B5',
|
||||
fontSize: 12
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLine: {
|
||||
show: false
|
||||
},
|
||||
axisLabel: {
|
||||
color: '#96A0B5',
|
||||
fontSize: 12
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: '#343744',
|
||||
type: 'dashed'
|
||||
}
|
||||
},
|
||||
min: 0,
|
||||
max: 100
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '充电量',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbol: 'none', // 默认不显示圆点
|
||||
symbolSize: 8,
|
||||
lineStyle: {
|
||||
width: 3,
|
||||
color: '#347AE2'
|
||||
},
|
||||
itemStyle: {
|
||||
color: '#347AE2',
|
||||
borderWidth: 2,
|
||||
borderColor: '#fff'
|
||||
},
|
||||
data: chartData.chargeData.map((value, index) => {
|
||||
// 只在最大值点显示圆点
|
||||
if (index === chargeMaxIndex) {
|
||||
return {
|
||||
value: value,
|
||||
symbolSize: 10,
|
||||
symbol: 'circle',
|
||||
itemStyle: {
|
||||
color: '#347AE2',
|
||||
borderWidth: 1,
|
||||
borderColor: '#fff',
|
||||
shadowColor: '#347AE2',
|
||||
shadowBlur: 10
|
||||
}
|
||||
};
|
||||
}
|
||||
return value;
|
||||
}),
|
||||
areaStyle: {
|
||||
opacity: 0.1,
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{
|
||||
offset: 0,
|
||||
color: '#347AE2'
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: 'rgba(52, 122, 226, 0)'
|
||||
}
|
||||
])
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '放电量',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbol: 'none', // 默认不显示圆点
|
||||
symbolSize: 8,
|
||||
lineStyle: {
|
||||
width: 3,
|
||||
color: '#B8D4FE'
|
||||
},
|
||||
itemStyle: {
|
||||
color: '#B8D4FE',
|
||||
borderWidth: 1,
|
||||
borderColor: '#fff',
|
||||
shadowColor: '#B8D4FE',
|
||||
shadowBlur: 10
|
||||
},
|
||||
data: chartData.dischargeData.map((value, index) => {
|
||||
// 只在最大值点显示圆点
|
||||
if (index === dischargeMaxIndex) {
|
||||
return {
|
||||
value: value,
|
||||
symbolSize: 10,
|
||||
symbol: 'circle',
|
||||
itemStyle: {
|
||||
color: '#B8D4FE',
|
||||
borderWidth: 2,
|
||||
borderColor: '#fff'
|
||||
}
|
||||
};
|
||||
}
|
||||
return value;
|
||||
}),
|
||||
areaStyle: {
|
||||
opacity: 0.1,
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{
|
||||
offset: 0,
|
||||
color: '#B8D4FE'
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: 'rgba(184, 212, 254, 0)'
|
||||
}
|
||||
])
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// 使用配置项设置图表
|
||||
chartInstance.setOption(option);
|
||||
|
||||
// 处理窗口大小变化
|
||||
window.addEventListener('resize', handleResize);
|
||||
}
|
||||
|
||||
// 处理窗口大小变化
|
||||
function handleResize() {
|
||||
if (chartInstance) {
|
||||
chartInstance.resize();
|
||||
}
|
||||
}
|
||||
|
||||
// 添加一个watch以监听容器变化
|
||||
const observer = ref(null);
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
// 使用ResizeObserver监听容器大小变化
|
||||
if (window.ResizeObserver && chartRef.value) {
|
||||
observer.value = new ResizeObserver(() => {
|
||||
if (chartInstance) {
|
||||
chartInstance.resize();
|
||||
} else {
|
||||
initChart();
|
||||
}
|
||||
});
|
||||
observer.value.observe(chartRef.value);
|
||||
}
|
||||
|
||||
// 延迟初始化以确保容器尺寸已计算
|
||||
setTimeout(() => {
|
||||
initChart();
|
||||
if (chartInstance) {
|
||||
chartInstance.resize();
|
||||
}
|
||||
}, 0);
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
if (observer.value && chartRef.value) {
|
||||
observer.value.unobserve(chartRef.value);
|
||||
observer.value = null;
|
||||
}
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose();
|
||||
chartInstance = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.chart-container {
|
||||
flex: 1;
|
||||
background: #343744;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 200px;
|
||||
|
||||
}
|
||||
|
||||
.chart-header {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chart-title {
|
||||
color: #4EA1FF;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.chart-center-legend {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
font-size: 12px;
|
||||
color: #96A0B5;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.legend-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.chart-body {
|
||||
flex: 1;
|
||||
// height: 180px;
|
||||
// min-height: 180px;
|
||||
}
|
||||
</style>
|
||||
269
src/components/car/detail/charts/ChargingDurationChart.vue
Normal file
@ -0,0 +1,269 @@
|
||||
<!-- 充电时长图表组件 -->
|
||||
<template>
|
||||
<div class="chart-container">
|
||||
<div class="chart-header">
|
||||
<div class="chart-title">充电时长分析</div>
|
||||
</div>
|
||||
<!-- <div class="chart-center-legend">
|
||||
<span class="legend-item">
|
||||
<span class="legend-color" style="background-color: #347AE2;"></span>
|
||||
<span>充电时长</span>
|
||||
</span>
|
||||
<span class="legend-item">
|
||||
<span class="legend-color" style="background-color: #B8D4FE;"></span>
|
||||
<span>放电时长</span>
|
||||
</span>
|
||||
<span class="chart-time-span">查看全部</span>
|
||||
</div> -->
|
||||
<div class="chart-body" ref="chartRef"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, nextTick, onBeforeUnmount } from 'vue';
|
||||
import * as echarts from 'echarts/core';
|
||||
import { BarChart } from 'echarts/charts';
|
||||
import { GridComponent, TooltipComponent, TitleComponent } from 'echarts/components';
|
||||
import { CanvasRenderer } from 'echarts/renderers';
|
||||
|
||||
// 注册必要的组件
|
||||
echarts.use([BarChart, GridComponent, TooltipComponent, TitleComponent, CanvasRenderer]);
|
||||
|
||||
const chartRef = ref(null);
|
||||
let chartInstance = null;
|
||||
|
||||
// 模拟数据
|
||||
const chartData = [
|
||||
{ month: '7.1', charge: 350, discharge: 280 },
|
||||
{ month: '7.2', charge: 230, discharge: 190 },
|
||||
{ month: '7.3', charge: 400, discharge: 320 },
|
||||
{ month: '7.4', charge: 280, discharge: 220 },
|
||||
{ month: '7.5', charge: 350, discharge: 260 },
|
||||
{ month: '7.6', charge: 450, discharge: 380 },
|
||||
{ month: '7.7', charge: 300, discharge: 240 },
|
||||
{ month: '7.8', charge: 420, discharge: 340 },
|
||||
{ month: '7.9', charge: 250, discharge: 180 },
|
||||
{ month: '7.10', charge: 380, discharge: 300 },
|
||||
{ month: '7.11', charge: 320, discharge: 250 },
|
||||
{ month: '7.12', charge: 400, discharge: 320 },
|
||||
];
|
||||
|
||||
// 初始化图表
|
||||
function initChart() {
|
||||
if (!chartRef.value) return;
|
||||
|
||||
// 创建图表实例
|
||||
chartInstance = echarts.init(chartRef.value);
|
||||
|
||||
// 设置图表配置项
|
||||
const option = {
|
||||
grid: {
|
||||
left: '2%',
|
||||
right: '2%',
|
||||
bottom: '10%',
|
||||
top: '5%',
|
||||
containLabel: true
|
||||
},
|
||||
legend: {
|
||||
data: ['充电时长', '放电时长'],
|
||||
top: '0',
|
||||
itemWidth: 10,
|
||||
itemHeight: 10,
|
||||
itemGap: 10,
|
||||
textStyle: {
|
||||
color: '#96A0B5',
|
||||
fontSize: 12
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
},
|
||||
backgroundColor: '#343744',
|
||||
borderColor: '#4C4F5F',
|
||||
textStyle: {
|
||||
color: '#fff'
|
||||
}
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: chartData.map(item => item.month),
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#4C4F5F'
|
||||
}
|
||||
},
|
||||
axisLabel: {
|
||||
color: '#96A0B5',
|
||||
fontSize: 12
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLine: {
|
||||
show: false
|
||||
},
|
||||
axisLabel: {
|
||||
color: '#96A0B5',
|
||||
fontSize: 12
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: '#343744',
|
||||
type: 'dashed'
|
||||
}
|
||||
},
|
||||
max: 500
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '充电时长',
|
||||
type: 'bar',
|
||||
barWidth: '20%',
|
||||
data: chartData.map(item => item.charge),
|
||||
itemStyle: {
|
||||
color: '#347AE2',
|
||||
borderRadius: [4, 4, 0, 0]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '放电时长',
|
||||
type: 'bar',
|
||||
barWidth: '20%',
|
||||
data: chartData.map(item => item.discharge),
|
||||
itemStyle: {
|
||||
color: '#B8D4FE',
|
||||
borderRadius: [4, 4, 0, 0]
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// 使用配置项设置图表
|
||||
chartInstance.setOption(option);
|
||||
|
||||
// 处理窗口大小变化
|
||||
window.addEventListener('resize', handleResize);
|
||||
}
|
||||
|
||||
// 处理窗口大小变化
|
||||
function handleResize() {
|
||||
if (chartInstance) {
|
||||
chartInstance.resize();
|
||||
}
|
||||
}
|
||||
|
||||
// 生命周期钩子
|
||||
onMounted(() => {
|
||||
// 确保DOM已完全渲染
|
||||
nextTick(() => {
|
||||
// 延迟初始化以确保容器尺寸已计算
|
||||
setTimeout(() => {
|
||||
initChart();
|
||||
if (chartInstance) {
|
||||
chartInstance.resize();
|
||||
}
|
||||
}, 0);
|
||||
});
|
||||
});
|
||||
|
||||
// 添加一个watch以监听容器变化
|
||||
const observer = ref(null);
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
// 使用ResizeObserver监听容器大小变化
|
||||
if (window.ResizeObserver && chartRef.value) {
|
||||
observer.value = new ResizeObserver(() => {
|
||||
if (chartInstance) {
|
||||
chartInstance.resize();
|
||||
} else {
|
||||
initChart();
|
||||
}
|
||||
});
|
||||
observer.value.observe(chartRef.value);
|
||||
}
|
||||
|
||||
// 初始化图表
|
||||
setTimeout(() => {
|
||||
initChart();
|
||||
}, 0);
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
if (observer.value && chartRef.value) {
|
||||
observer.value.unobserve(chartRef.value);
|
||||
observer.value = null;
|
||||
}
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose();
|
||||
chartInstance = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.chart-container {
|
||||
flex: 1;
|
||||
background: #343744;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.chart-header {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chart-title {
|
||||
color: #4EA1FF;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.chart-center-legend {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 12px;
|
||||
color: #96A0B5;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.legend-color {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.chart-time-span {
|
||||
color: #347AE2;
|
||||
cursor: pointer;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.chart-body {
|
||||
flex: 1;
|
||||
// height: 180px;
|
||||
// border:1px solid red;
|
||||
// min-height: 180px;
|
||||
}
|
||||
</style>
|
||||
@ -8,10 +8,13 @@
|
||||
<OpenLayersZoomControl
|
||||
:map="map"
|
||||
:resetView="resetView"
|
||||
:vehicleCategories="vehicleCategories"
|
||||
:vehicleMovementControl="vehicleMovementControl"
|
||||
@compass="compass"
|
||||
@zoomIn="zoomIn"
|
||||
@zoomOut="zoomOut"
|
||||
@layerChange="handleLayerChange"
|
||||
@setCategoryVisibility="handleSetCategoryVisibility"
|
||||
/>
|
||||
|
||||
<!-- 地图信息 -->
|
||||
@ -50,6 +53,26 @@ import ScaleLine from 'ol/control/ScaleLine';
|
||||
import OpenLayersScaleControl from "./controls/OpenLayersScaleControl.vue";
|
||||
import RouteDrawControl from "./controls/RouteDrawControl.vue";
|
||||
|
||||
// 接收props
|
||||
const props = defineProps({
|
||||
vehicleCategories: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
vehicleMovementControl: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
});
|
||||
|
||||
// 定义事件
|
||||
const emit = defineEmits(['setCategoryVisibility']);
|
||||
|
||||
// 处理设置分类可见性
|
||||
function handleSetCategoryVisibility(type, settings) {
|
||||
emit('setCategoryVisibility', type, settings);
|
||||
}
|
||||
|
||||
// 注册 EPSG:4528 投影
|
||||
proj4.defs(
|
||||
"EPSG:4528",
|
||||
@ -344,7 +367,8 @@ defineExpose({
|
||||
zoomOut,
|
||||
compass,
|
||||
resetView,
|
||||
getCurrentMapState // 暴露获取当前地图状态的方法
|
||||
getCurrentMapState, // 暴露获取当前地图状态的方法
|
||||
map // 暴露地图实例
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
1180
src/components/map/controls/LayerSwitcher copy.vue
Normal file
@ -13,13 +13,13 @@
|
||||
>
|
||||
图标
|
||||
</div>
|
||||
<!-- <div
|
||||
<div
|
||||
class="tab"
|
||||
:class="{ active: activeTab === 'text' }"
|
||||
@click="activeTab = 'text'"
|
||||
>
|
||||
文本
|
||||
</div> -->
|
||||
</div>
|
||||
<div
|
||||
class="tab"
|
||||
:class="{ active: activeTab === 'road' }"
|
||||
@ -29,82 +29,40 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图标tab页 -->
|
||||
<div class="panel-content" v-if="activeTab === 'icon'">
|
||||
|
||||
<!-- 航空器图层 -->
|
||||
<div class="layer-group">
|
||||
<div class="group-title">航空器</div>
|
||||
<!-- <div class="group-title">图标显示控制</div> -->
|
||||
<div class="layer-grid">
|
||||
<div class="layer-item" v-for="layer in exampleVehicleLayers.filter(l => l.id !== 'no-people-car')" :key="'example-'+layer.id">
|
||||
<div class="layer-item" v-for="(cat, type) in categories" :key="type">
|
||||
<label class="checkbox-container">
|
||||
<input type="checkbox" :checked="layer.visible" @change="toggleExampleLayer(layer)">
|
||||
<input type="checkbox" v-model="cat.visible" @change="emitSet(type)">
|
||||
<span class="checkmark"></span>
|
||||
<span class="layer-name">{{ layer.name }}</span>
|
||||
<img :src="layer.icon" class="layer-icon-preview" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 无人车全选 -->
|
||||
<div class="layer-group">
|
||||
<div class="group-title">无人车</div>
|
||||
<div class="layer-grid-full">
|
||||
<div class="layer-item">
|
||||
<label class="checkbox-container">
|
||||
<input type="checkbox" :checked="getExampleLayerById('no-people-car').visible" @change="toggleExampleLayer(getExampleLayerById('no-people-car'))">
|
||||
<span class="checkmark"></span>
|
||||
<span class="layer-name">无人车全选</span>
|
||||
<img :src="noPeopleCarIcon" class="layer-icon-preview" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 常用车辆 -->
|
||||
<div class="layer-group">
|
||||
<div class="group-title">常用车辆</div>
|
||||
<div class="layer-grid">
|
||||
<div class="layer-item">
|
||||
<label class="checkbox-container">
|
||||
<input type="checkbox" :checked="getLayerById('driving-car').visible" @change="toggleVehicleLayer(getLayerById('driving-car'))">
|
||||
<span class="checkmark"></span>
|
||||
<span class="layer-name">驱鸟车</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="layer-item">
|
||||
<label class="checkbox-container">
|
||||
<input type="checkbox" :checked="getLayerById('push-car').visible" @change="toggleVehicleLayer(getLayerById('push-car'))">
|
||||
<span class="checkmark"></span>
|
||||
<span class="layer-name">摆渡车</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="layer-item">
|
||||
<label class="checkbox-container">
|
||||
<input type="checkbox" :checked="getLayerById('military-car').visible" @change="toggleVehicleLayer(getLayerById('military-car'))">
|
||||
<span class="checkmark"></span>
|
||||
<span class="layer-name">牵引车</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 其他车辆图层 -->
|
||||
<div class="layer-group">
|
||||
<div class="group-title">其他车辆</div>
|
||||
<div class="layer-grid">
|
||||
<div class="layer-item" v-for="layer in otherVehicleLayers" :key="'vehicle-'+layer.id">
|
||||
<label class="checkbox-container">
|
||||
<input type="checkbox" :checked="layer.visible" @change="toggleVehicleLayer(layer)">
|
||||
<span class="checkmark"></span>
|
||||
<span class="layer-name">{{ layer.name }}</span>
|
||||
<span class="layer-name">{{ cat.name }}</span>
|
||||
<!-- <img :src="cat.icon" class="layer-icon-preview" /> -->
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-content" v-else-if="activeTab === 'road'">
|
||||
<!-- 文本tab页 -->
|
||||
<div class="panel-content" v-else-if="activeTab === 'text'">
|
||||
<div class="layer-group">
|
||||
<!-- <div class="group-title">标签显示控制</div> -->
|
||||
<div class="layer-grid">
|
||||
<div class="layer-item" v-for="(cat, type) in categories" :key="type">
|
||||
<label class="checkbox-container">
|
||||
<input type="checkbox" v-model="cat.showLabel" @change="emitSet(type)">
|
||||
<span class="checkmark"></span>
|
||||
<span class="layer-name">{{ cat.name }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-content" v-else>
|
||||
<div class="layer-group">
|
||||
<div class="group-title">道路图层</div>
|
||||
<div class="layer-grid-full">
|
||||
@ -113,7 +71,7 @@
|
||||
<input type="checkbox" :checked="!hideRoadLayer" @change="toggleHideRoadLayer" />
|
||||
<span class="checkmark"></span>
|
||||
<span class="layer-name">电子围栏</span>
|
||||
<svg class="layer-icon-preview" width="24" height="24" viewBox="0 0 1024 1024"><path d="M356.246145 681.56286c-68.156286-41.949414-107.246583-103.84102-107.246583-169.805384 0-65.966411 39.090297-127.860063 107.246583-169.809477 12.046361-7.414877 15.800871-23.190165 8.385994-35.236526-7.413853-12.046361-23.191188-15.801894-35.236526-8.387018-39.640836 24.399713-72.539106 56.044434-95.137801 91.515297-23.86657 37.461193-36.481889 79.620385-36.481889 121.917724 0 42.297338 12.615319 84.454484 36.481889 121.914654 22.598694 35.469839 55.496965 67.11456 95.137801 91.51325 4.185322 2.576685 8.821923 3.804652 13.400195 3.804652 8.598842 0 16.998139-4.329609 21.836331-12.190647C372.047016 704.752002 368.291482 688.976714 356.246145 681.56286z" fill="#409eff"/></svg>
|
||||
<!-- <svg class="layer-icon-preview" width="24" height="24" viewBox="0 0 1024 1024"><path d="M356.246145 681.56286c-68.156286-41.949414-107.246583-103.84102-107.246583-169.805384 0-65.966411 39.090297-127.860063 107.246583-169.809477 12.046361-7.414877 15.800871-23.190165 8.385994-35.236526-7.413853-12.046361-23.191188-15.801894-35.236526-8.387018-39.640836 24.399713-72.539106 56.044434-95.137801 91.515297-23.86657 37.461193-36.481889 79.620385-36.481889 121.917724 0 42.297338 12.615319 84.454484 36.481889 121.914654 22.598694 35.469839 55.496965 67.11456 95.137801 91.51325 4.185322 2.576685 8.821923 3.804652 13.400195 3.804652 8.598842 0 16.998139-4.329609 21.836331-12.190647C372.047016 704.752002 368.291482 688.976714 356.246145 681.56286z" fill="#409eff"/></svg> -->
|
||||
</label>
|
||||
</div>
|
||||
<!-- 添加自定义路线图层选项 -->
|
||||
@ -122,33 +80,12 @@
|
||||
<input type="checkbox" v-model="showCustomRoadLayer" />
|
||||
<span class="checkmark"></span>
|
||||
<span class="layer-name">路线图</span>
|
||||
<svg class="layer-icon-preview" width="24" height="24" viewBox="0 0 1024 1024"><path d="M356.246145 681.56286c-68.156286-41.949414-107.246583-103.84102-107.246583-169.805384 0-65.966411 39.090297-127.860063 107.246583-169.809477 12.046361-7.414877 15.800871-23.190165 8.385994-35.236526-7.413853-12.046361-23.191188-15.801894-35.236526-8.387018-39.640836 24.399713-72.539106 56.044434-95.137801 91.515297-23.86657 37.461193-36.481889 79.620385-36.481889 121.917724 0 42.297338 12.615319 84.454484 36.481889 121.914654 22.598694 35.469839 55.496965 67.11456 95.137801 91.51325 4.185322 2.576685 8.821923 3.804652 13.400195 3.804652 8.598842 0 16.998139-4.329609 21.836331-12.190647C372.047016 704.752002 368.291482 688.976714 356.246145 681.56286z" fill="#FF5722"/></svg>
|
||||
<!-- <svg class="layer-icon-preview" width="24" height="24" viewBox="0 0 1024 1024"><path d="M356.246145 681.56286c-68.156286-41.949414-107.246583-103.84102-107.246583-169.805384 0-65.966411 39.090297-127.860063 107.246583-169.809477 12.046361-7.414877 15.800871-23.190165 8.385994-35.236526-7.413853-12.046361-23.191188-15.801894-35.236526-8.387018-39.640836 24.399713-72.539106 56.044434-95.137801 91.515297-23.86657 37.461193-36.481889 79.620385-36.481889 121.917724 0 42.297338 12.615319 84.454484 36.481889 121.914654 22.598694 35.469839 55.496965 67.11456 95.137801 91.51325 4.185322 2.576685 8.821923 3.804652 13.400195 3.804652 8.598842 0 16.998139-4.329609 21.836331-12.190647C372.047016 704.752002 368.291482 688.976714 356.246145 681.56286z" fill="#FF5722"/></svg> -->
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-content" v-else>
|
||||
<!-- 文本标签页内容 -->
|
||||
<div class="layer-group">
|
||||
<div class="group-title">文本样式</div>
|
||||
<div class="style-selector">
|
||||
<div class="style-item">
|
||||
<div class="style-label">默认样式</div>
|
||||
<div class="radio-box" :class="{ active: selectedTextStyle === 'default' }" @click="selectTextStyle('default')"></div>
|
||||
</div>
|
||||
<div class="style-item">
|
||||
<div class="style-label">蓝色样式</div>
|
||||
<div class="radio-box blue" :class="{ active: selectedTextStyle === 'blue' }" @click="selectTextStyle('blue')"></div>
|
||||
</div>
|
||||
<div class="style-item">
|
||||
<div class="style-label">白色样式</div>
|
||||
<div class="radio-box white" :class="{ active: selectedTextStyle === 'white' }" @click="selectTextStyle('white')"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -161,15 +98,21 @@ import { Style, Icon, Stroke, Fill } from 'ol/style';
|
||||
import Feature from 'ol/Feature';
|
||||
import Point from 'ol/geom/Point';
|
||||
import GeoJSON from 'ol/format/GeoJSON';
|
||||
import Overlay from 'ol/Overlay';
|
||||
import labelBg from '../../../assets/images/label_bg.png';
|
||||
|
||||
// 导入车辆图标
|
||||
import car1Icon from '../../../assets/images/Aircraft.png';
|
||||
import car1Icon from '../../../assets/images/Aircraft.png'; //滑入航空器
|
||||
import car1Icon1 from '../../../assets/images/Aircraft1.png'; //滑出航空器
|
||||
import car2Icon from '../../../assets/images/noPeopleCar.png';
|
||||
import noPeopleCarIcon from '../../../assets/images/noPeopleCar.png';
|
||||
import airportBg from '../../../assets/images/airport_bg.png'; // 蓝色背景
|
||||
import airportOutBg from '../../../assets/images/airport_out.png'; // 黄色背景
|
||||
|
||||
// 定义props接收地图实例
|
||||
const props = defineProps({
|
||||
map: Object
|
||||
map: Object,
|
||||
categories: Object
|
||||
});
|
||||
|
||||
// 响应式状态
|
||||
@ -186,243 +129,14 @@ let roadVectorLayer = null;
|
||||
const showCustomRoadLayer = ref(true); // 默认显示
|
||||
let customRoadVectorLayer = null;
|
||||
|
||||
// 车辆图层 - 默认选中
|
||||
const exampleVehicleLayers = ref([
|
||||
{
|
||||
id: 'car1-layer',
|
||||
name: '滑入航空器',
|
||||
visible: true,
|
||||
icon: car1Icon,
|
||||
layer: null,
|
||||
features: [
|
||||
{ id: 'car1-1', position: [40507699.051041, 4026243.105796], name: '滑入航空器1' },
|
||||
{ id: 'car1-2', position: [40508097.909698, 4026315.085762], name: '滑入航空器2' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'car1-layer1',
|
||||
name: '滑出航空器',
|
||||
visible: true,
|
||||
icon: car1Icon,
|
||||
layer: null,
|
||||
features: [
|
||||
{ id: 'car2-1', position: [4.0507e7, 4024000], name: '滑出航空器1' },
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'no-people-car',
|
||||
name: '无人车全选',
|
||||
visible: true,
|
||||
icon: noPeopleCarIcon,
|
||||
layer: null,
|
||||
features: [
|
||||
{ id: 'no-people-car-1', position: [40508392.141630, 4026279.431719], name: '无人车1' },
|
||||
{ id: 'no-people-car-2', position: [40507625.995559, 4025622.462289], name: '无人车2' }
|
||||
]
|
||||
}
|
||||
]);
|
||||
|
||||
// 车辆图层 - 设置默认显示状态
|
||||
const vehicleLayers = ref([
|
||||
{ id: 'slip-in-plane', name: '滑入航空器', visible: true, layer: null },
|
||||
{ id: 'slip-out-plane', name: '滑出航空器', visible: true, layer: null },
|
||||
{ id: 'uav', name: '无人车全选', visible: true, layer: null },
|
||||
{ id: 'driving-car', name: '驱鸟车', visible: true, layer: null },
|
||||
{ id: 'push-car', name: '摆渡车', visible: true, layer: null },
|
||||
{ id: 'military-car', name: '牵引车', visible: true, layer: null },
|
||||
{ id: 'special-car', name: '特勤车全选', visible: false, layer: null },
|
||||
{ id: 'fire-car', name: '消防车', visible: false, layer: null },
|
||||
{ id: 'water-car', name: '清水车', visible: false, layer: null },
|
||||
{ id: 'patrol-car', name: '巡逻车', visible: false, layer: null },
|
||||
{ id: 'emergency-car', name: '急救车', visible: false, layer: null },
|
||||
{ id: 'container-car', name: '客梯车', visible: false, layer: null },
|
||||
{ id: 'small-car', name: '小型客车', visible: false, layer: null },
|
||||
{ id: 'maintenance-car', name: '维修车', visible: false, layer: null },
|
||||
{ id: 'tool-car', name: '工具车', visible: false, layer: null },
|
||||
{ id: 'tour-car', name: '巡游车', visible: false, layer: null },
|
||||
{ id: 'sewage-car', name: '污水车', visible: false, layer: null },
|
||||
{ id: 'road-car', name: '道路车', visible: false, layer: null },
|
||||
{ id: 'garbage-car', name: '垃圾车', visible: false, layer: null },
|
||||
{ id: 'police-car', name: '警察车', visible: false, layer: null },
|
||||
{ id: 'forklift', name: '叉车', visible: false, layer: null }
|
||||
]);
|
||||
|
||||
// 文本样式
|
||||
const textStyles = ref([
|
||||
{ id: 'slip-in-plane', name: '滑入航空器', visible: true, layer: null },
|
||||
{ id: 'slip-out-plane', name: '滑出航空器', visible: true, layer: null },
|
||||
{ id: 'uav', name: '无人车全选', visible: true, layer: null },
|
||||
{ id: 'driving-car', name: '驱鸟车', visible: true, layer: null },
|
||||
{ id: 'push-car', name: '摆渡车', visible: true, layer: null },
|
||||
{ id: 'military-car', name: '军引车', visible: true, layer: null },
|
||||
{ id: 'special-car', name: '特勤车全选', visible: false, layer: null },
|
||||
{ id: 'fire-car', name: '消防车', visible: false, layer: null },
|
||||
]);
|
||||
|
||||
// 计算属性:其他车辆图层(排除常用车辆)
|
||||
const otherVehicleLayers = computed(() => {
|
||||
const excludedIds = ['uav', 'driving-car', 'push-car', 'military-car', 'slip-in-plane', 'slip-out-plane'];
|
||||
return vehicleLayers.value.filter(layer => !excludedIds.includes(layer.id));
|
||||
});
|
||||
|
||||
// 通过ID获取图层
|
||||
function getLayerById(id) {
|
||||
return vehicleLayers.value.find(layer => layer.id === id) || { visible: false };
|
||||
}
|
||||
|
||||
// 通过ID获取示例图层
|
||||
function getExampleLayerById(id) {
|
||||
return exampleVehicleLayers.value.find(layer => layer.id === id) || { visible: false };
|
||||
}
|
||||
|
||||
// 向父组件发出事件
|
||||
const emit = defineEmits(['layerChange']);
|
||||
const emit = defineEmits(['layerChange', 'setCategoryVisibility']);
|
||||
|
||||
// 切换图层面板显示
|
||||
function toggleLayerPanel() {
|
||||
showPanel.value = !showPanel.value;
|
||||
}
|
||||
|
||||
// 切换示例图层
|
||||
function toggleExampleLayer(layer) {
|
||||
layer.visible = !layer.visible;
|
||||
|
||||
if (layer.layer) {
|
||||
layer.layer.setVisible(layer.visible);
|
||||
}
|
||||
|
||||
// 发出图层变化事件
|
||||
emit('layerChange', {
|
||||
type: 'exampleLayer',
|
||||
layer: layer
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化示例图层
|
||||
function initExampleLayers() {
|
||||
if (!props.map) return;
|
||||
|
||||
// 为每个示例图层创建矢量图层
|
||||
exampleVehicleLayers.value.forEach(layerConfig => {
|
||||
// 创建矢量数据源
|
||||
const source = new VectorSource();
|
||||
|
||||
// 创建样式
|
||||
const style = new Style({
|
||||
image: new Icon({
|
||||
src: layerConfig.icon,
|
||||
scale: 1.5,
|
||||
anchor: [0.5, 0.5]
|
||||
})
|
||||
});
|
||||
|
||||
// 添加车辆特征
|
||||
layerConfig.features.forEach(vehicle => {
|
||||
const feature = new Feature({
|
||||
geometry: new Point(vehicle.position),
|
||||
name: vehicle.name,
|
||||
id: vehicle.id
|
||||
});
|
||||
|
||||
feature.setStyle(style);
|
||||
source.addFeature(feature);
|
||||
});
|
||||
|
||||
// 创建矢量图层
|
||||
const vectorLayer = new VectorLayer({
|
||||
source: source,
|
||||
visible: layerConfig.visible,
|
||||
zIndex: 10 // 确保在基础图层之上
|
||||
});
|
||||
|
||||
// 设置图层引用
|
||||
layerConfig.layer = vectorLayer;
|
||||
|
||||
// 添加到地图
|
||||
props.map.addLayer(vectorLayer);
|
||||
|
||||
// 确保可见性设置正确
|
||||
vectorLayer.setVisible(layerConfig.visible);
|
||||
|
||||
console.log(`初始化图层: ${layerConfig.id}, 可见性: ${layerConfig.visible}`);
|
||||
});
|
||||
}
|
||||
|
||||
// 切换业务图层
|
||||
function toggleVehicleLayer(layer) {
|
||||
layer.visible = !layer.visible;
|
||||
|
||||
if (layer.layer) {
|
||||
layer.layer.setVisible(layer.visible);
|
||||
}
|
||||
|
||||
// 发出图层变化事件
|
||||
emit('layerChange', {
|
||||
type: 'vehicleLayer',
|
||||
layer: layer
|
||||
});
|
||||
}
|
||||
|
||||
// 切换文本样式
|
||||
function toggleTextStyle(textStyle) {
|
||||
textStyle.visible = !textStyle.visible;
|
||||
|
||||
// 发出文本样式变化事件
|
||||
emit('layerChange', {
|
||||
type: 'textStyle',
|
||||
style: textStyle
|
||||
});
|
||||
}
|
||||
|
||||
// 选择文本样式
|
||||
function selectTextStyle(style) {
|
||||
selectedTextStyle.value = style;
|
||||
|
||||
// 发出文本样式选择事件
|
||||
emit('layerChange', {
|
||||
type: 'selectTextStyle',
|
||||
style: style
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化图层
|
||||
function initLayers() {
|
||||
if (!props.map) return;
|
||||
|
||||
// 初始化示例图层
|
||||
initExampleLayers();
|
||||
}
|
||||
|
||||
// 设置图层可见性的方法(供外部调用)
|
||||
function setLayerVisibility(layerId, visible) {
|
||||
// 查找并设置示例图层可见性
|
||||
const exampleLayer = exampleVehicleLayers.value.find(layer => layer.id === layerId);
|
||||
if (exampleLayer) {
|
||||
exampleLayer.visible = visible;
|
||||
if (exampleLayer.layer) {
|
||||
exampleLayer.layer.setVisible(visible);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 查找并设置车辆图层可见性
|
||||
const vehicleLayer = vehicleLayers.value.find(layer => layer.id === layerId);
|
||||
if (vehicleLayer) {
|
||||
vehicleLayer.visible = visible;
|
||||
|
||||
// 发出图层变化事件
|
||||
emit('layerChange', {
|
||||
type: 'vehicleLayer',
|
||||
layer: vehicleLayer
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// 切换电子围栏显示状态
|
||||
function toggleHideRoadLayer() {
|
||||
hideRoadLayer.value = !hideRoadLayer.value;
|
||||
@ -438,9 +152,9 @@ function toggleHideRoadLayer() {
|
||||
// 组件挂载时初始化
|
||||
onMounted(() => {
|
||||
if (props.map) {
|
||||
initLayers();
|
||||
// 初始化自定义路线图层
|
||||
loadCustomRoadLayer();
|
||||
|
||||
// 初始化电子围栏图层 - 默认显示
|
||||
if (showRoadLayer.value) {
|
||||
addRoadLayer();
|
||||
@ -451,9 +165,9 @@ onMounted(() => {
|
||||
// 监听地图实例变化
|
||||
watch(() => props.map, (newMap) => {
|
||||
if (newMap) {
|
||||
initLayers();
|
||||
// 初始化自定义路线图层
|
||||
loadCustomRoadLayer();
|
||||
|
||||
// 初始化电子围栏图层
|
||||
if (showRoadLayer.value) {
|
||||
addRoadLayer();
|
||||
@ -463,7 +177,12 @@ watch(() => props.map, (newMap) => {
|
||||
|
||||
// 向外暴露方法
|
||||
defineExpose({
|
||||
setLayerVisibility
|
||||
setLayerVisibility(typeKey, visible) {
|
||||
emit('setCategoryVisibility', typeKey, {
|
||||
visible,
|
||||
showLabel: props.categories[typeKey]?.showLabel || true
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
async function loadCustomRoadLayer() {
|
||||
@ -635,6 +354,14 @@ onUnmounted(() => {
|
||||
removeCustomRoadLayer();
|
||||
stopFenceFlashing();
|
||||
});
|
||||
|
||||
// 发送分类可见性设置到父组件
|
||||
function emitSet(type) {
|
||||
emit('setCategoryVisibility', type, {
|
||||
visible: props.categories[type].visible,
|
||||
showLabel: props.categories[type].showLabel
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -643,8 +370,8 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.layer-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@ -798,8 +525,8 @@ onUnmounted(() => {
|
||||
|
||||
/* 图层图标预览 */
|
||||
.layer-icon-preview {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-left: 8px;
|
||||
object-fit: contain;
|
||||
}
|
||||
@ -901,4 +628,63 @@ onUnmounted(() => {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* 自定义标签样式 */
|
||||
.custom-label {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 120px;
|
||||
height: 28px;
|
||||
padding: 0 10px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
border-radius: 4px;
|
||||
border: 1px solid;
|
||||
color: #fff;
|
||||
box-sizing: border-box;
|
||||
white-space: nowrap;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
/* 滑入航空器 - 黄色背景,黑色文字 */
|
||||
.label-aircraft-in {
|
||||
background-color: rgba(245, 231, 79, 0.7);
|
||||
border-color: #E4CB0D;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 滑出航空器 - 蓝色背景,白色文字 */
|
||||
.label-aircraft-out {
|
||||
background-color: rgba(52, 122, 226, 0.7);
|
||||
border-color: #347AE2;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 无人车 */
|
||||
.label-car {
|
||||
background-color: rgba(37, 37, 37, 0.7);
|
||||
border-color: #484848;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 文本样式类 */
|
||||
.custom-label.style-default {
|
||||
/* 默认样式在各个类型中已定义 */
|
||||
}
|
||||
|
||||
.custom-label.style-blue {
|
||||
background-color: rgba(52, 122, 226, 0.7) !important;
|
||||
border-color: #347AE2 !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.custom-label.style-white {
|
||||
background-color: rgba(255, 255, 255, 0.7) !important;
|
||||
border-color: #ffffff !important;
|
||||
color: #333 !important;
|
||||
}
|
||||
</style>
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="map-info">
|
||||
<!-- <input type="text" readonly :value="xyText" />
|
||||
<input type="text" readonly :value="wgs84Text" /> -->
|
||||
<input type="text" readonly :value="xyText" />
|
||||
<input type="text" readonly :value="wgs84Text" />
|
||||
<!-- <input type="text" readonly :value="pixelText" /> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -1,22 +1,50 @@
|
||||
<template>
|
||||
<div class="zoom-control">
|
||||
<LayerSwitcher class="layer-switcher" :map="map" @layerChange="onLayerChange" ref="layerSwitcherRef" />
|
||||
|
||||
<div class="compass" @click="onCompass"></div>
|
||||
<LayerSwitcher
|
||||
class="layer-switcher"
|
||||
:map="map"
|
||||
:categories="vehicleCategories"
|
||||
@layerChange="onLayerChange"
|
||||
@setCategoryVisibility="handleSetCategoryVisibility"
|
||||
ref="layerSwitcherRef"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="compass-container"
|
||||
:style="{ transform: `rotate(${-rotation}deg)` }"
|
||||
>
|
||||
<div class="rotate-left" @click="rotateLeft" title="向右旋转90°"></div>
|
||||
|
||||
<div class="rotate-right" @click="rotateRight" title="向左旋转90°"></div>
|
||||
</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';
|
||||
import { ref, onMounted, onUnmounted, watch } from "vue";
|
||||
import LayerSwitcher from "./LayerSwitcher.vue";
|
||||
|
||||
// 导入图片资源
|
||||
import znzBgImg from "../../../assets/images/znzBg.png";
|
||||
import needleImg from "../../../assets/images/znz.png";
|
||||
import leftArrowImg from "../../../assets/images/left_arrow.png";
|
||||
import rightArrowImg from "../../../assets/images/right_arrow.png";
|
||||
|
||||
// 定义props接受地图实例
|
||||
const props = defineProps({
|
||||
map: Object, // 地图实例
|
||||
resetView: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
vehicleCategories: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
vehicleMovementControl: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
});
|
||||
@ -24,12 +52,68 @@ const props = defineProps({
|
||||
// 组件引用
|
||||
const layerSwitcherRef = ref(null);
|
||||
|
||||
// 地图旋转角度 (弧度转换为度)
|
||||
const rotation = ref(0);
|
||||
|
||||
// 向父组件发出事件
|
||||
const emit = defineEmits(['compass', 'zoomIn', 'zoomOut', 'layerChange']);
|
||||
const emit = defineEmits([
|
||||
"compass",
|
||||
"zoomIn",
|
||||
"zoomOut",
|
||||
"layerChange",
|
||||
"rotate",
|
||||
"setCategoryVisibility"
|
||||
]);
|
||||
|
||||
// 监听地图视图变化,更新旋转角度
|
||||
function updateRotation() {
|
||||
if (props.map && props.map.getView()) {
|
||||
// OpenLayers中旋转角度是弧度,转换为度
|
||||
rotation.value = (props.map.getView().getRotation() * 180) / Math.PI;
|
||||
}
|
||||
}
|
||||
|
||||
// 重置地图旋转
|
||||
function resetRotation() {
|
||||
if (props.map) {
|
||||
// 重置旋转为0
|
||||
props.map.getView().animate({
|
||||
rotation: 0,
|
||||
duration: 300,
|
||||
});
|
||||
emit("rotate", 0);
|
||||
}
|
||||
}
|
||||
|
||||
// 向左旋转地图(逆时针)
|
||||
function rotateLeft() {
|
||||
if (props.map) {
|
||||
const currentRotation = props.map.getView().getRotation();
|
||||
const newRotation = currentRotation - Math.PI / 2; // 旋转90度(逆时针)
|
||||
props.map.getView().animate({
|
||||
rotation: newRotation,
|
||||
duration: 300,
|
||||
});
|
||||
emit("rotate", (newRotation * 180) / Math.PI);
|
||||
}
|
||||
}
|
||||
|
||||
// 向右旋转地图(顺时针)
|
||||
function rotateRight() {
|
||||
if (props.map) {
|
||||
const currentRotation = props.map.getView().getRotation();
|
||||
const newRotation = currentRotation + Math.PI / 2; // 旋转90度(顺时针)
|
||||
props.map.getView().animate({
|
||||
rotation: newRotation,
|
||||
duration: 300,
|
||||
});
|
||||
emit("rotate", (newRotation * 180) / Math.PI);
|
||||
}
|
||||
}
|
||||
|
||||
// 指南针/重置视图
|
||||
function onCompass() {
|
||||
emit('compass');
|
||||
emit("compass");
|
||||
if (props.resetView) {
|
||||
props.resetView();
|
||||
} else if (props.map) {
|
||||
@ -42,21 +126,104 @@ function onCompass() {
|
||||
|
||||
// 放大地图
|
||||
function onZoomIn() {
|
||||
emit('zoomIn');
|
||||
emit("zoomIn");
|
||||
if (props.map) {
|
||||
const currentZoom = props.map.getView().getZoom();
|
||||
props.map.getView().animate({
|
||||
zoom: currentZoom + 1,
|
||||
duration: 250,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 缩小地图
|
||||
function onZoomOut() {
|
||||
emit('zoomOut');
|
||||
emit("zoomOut");
|
||||
if (props.map) {
|
||||
const currentZoom = props.map.getView().getZoom();
|
||||
props.map.getView().animate({
|
||||
zoom: currentZoom - 1,
|
||||
duration: 250,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 图层变化处理
|
||||
function onLayerChange(layerInfo) {
|
||||
console.log('ZoomControl收到图层变化:', layerInfo);
|
||||
console.log("ZoomControl收到图层变化:", layerInfo);
|
||||
// 将图层信息传递给父组件
|
||||
emit('layerChange', layerInfo);
|
||||
emit("layerChange", layerInfo);
|
||||
}
|
||||
|
||||
// 处理分类可见性设置
|
||||
function handleSetCategoryVisibility(type, settings) {
|
||||
// 发送到父组件
|
||||
emit('setCategoryVisibility', type, settings);
|
||||
|
||||
// 如果props中有vehicleMovementControl,直接调用其方法
|
||||
if (props.vehicleMovementControl && props.vehicleMovementControl.setCategoryVisibility) {
|
||||
props.vehicleMovementControl.setCategoryVisibility(type, settings);
|
||||
}
|
||||
}
|
||||
|
||||
// 监听地图旋转事件
|
||||
let rotationListener = null;
|
||||
|
||||
onMounted(() => {
|
||||
if (props.map) {
|
||||
// 初始化旋转角度
|
||||
updateRotation();
|
||||
|
||||
// 添加视图变化监听器 - 使用on方法返回的事件键
|
||||
rotationListener = props.map
|
||||
.getView()
|
||||
.on("change:rotation", updateRotation);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
// 移除监听器 - 使用OpenLayers的事件解绑方式
|
||||
if (rotationListener && props.map) {
|
||||
try {
|
||||
// 正确方式是使用unByKey方法解绑事件
|
||||
import('ol/Observable').then(({ unByKey }) => {
|
||||
unByKey(rotationListener);
|
||||
}).catch(e => {
|
||||
console.error('清理事件监听器失败:', e);
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('清理事件监听器失败:', e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 监听地图实例变化
|
||||
watch(
|
||||
() => props.map,
|
||||
(newMap) => {
|
||||
if (newMap) {
|
||||
// 初始化旋转角度
|
||||
updateRotation();
|
||||
|
||||
// 移除旧的监听器
|
||||
if (rotationListener) {
|
||||
try {
|
||||
import('ol/Observable').then(({ unByKey }) => {
|
||||
unByKey(rotationListener);
|
||||
}).catch(e => {
|
||||
console.error('清理事件监听器失败:', e);
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('清理事件监听器失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加新的视图变化监听器
|
||||
rotationListener = newMap.getView().on("change:rotation", updateRotation);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 导出方法供外部调用
|
||||
defineExpose({
|
||||
// 设置图层可见性的方法
|
||||
@ -65,7 +232,13 @@ defineExpose({
|
||||
return layerSwitcherRef.value.setLayerVisibility(layerName, visible);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
},
|
||||
// 重置旋转
|
||||
resetRotation,
|
||||
// 获取当前旋转角度
|
||||
getRotation() {
|
||||
return rotation.value;
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -83,15 +256,64 @@ defineExpose({
|
||||
gap: 25px;
|
||||
}
|
||||
|
||||
.compass {
|
||||
.compass-container {
|
||||
position: relative;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: url("../../../assets/images/znz.png") no-repeat;
|
||||
background-size: 100% 100%;
|
||||
cursor: pointer;
|
||||
z-index: 3000;
|
||||
background: url("../../../assets/images/znzBg.png") no-repeat;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
.layer-switcher{
|
||||
|
||||
.rotation-controls {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
z-index: 3002;
|
||||
}
|
||||
|
||||
.rotate-left,
|
||||
.rotate-right {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.rotate-left {
|
||||
left: -3px;
|
||||
background: url("../../../assets/images/left_arrow.png") no-repeat;
|
||||
background-size: 100% 100%;
|
||||
z-index: 5000;
|
||||
}
|
||||
|
||||
.rotate-right {
|
||||
right: -3px;
|
||||
background: url("../../../assets/images/right_arrow.png") no-repeat;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.rotate-left:hover,
|
||||
.rotate-left:active {
|
||||
left: -3px;
|
||||
background: url("../../../assets/images/left_arrow_active.png") no-repeat;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
.rotate-right:hover,
|
||||
.rotate-right:active {
|
||||
right: -3px;
|
||||
background: url("../../../assets/images/right_arrow_active.png") no-repeat;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.layer-switcher {
|
||||
z-index: 3000;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
@ -99,6 +321,12 @@ defineExpose({
|
||||
background-size: 100% 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
.layer-switcher:hover,
|
||||
.layer-switcher.active {
|
||||
background: url("../../../assets/images/layerActive.png") no-repeat;
|
||||
background-size: 100% 100%;
|
||||
opacity: 1; /* 确保不透明度为1 */
|
||||
}
|
||||
|
||||
.zoom-in {
|
||||
background: url("../../../assets/images/zoomOut.png") no-repeat;
|
||||
@ -108,6 +336,11 @@ defineExpose({
|
||||
height: 40px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.zoom-in:hover,
|
||||
.zoom-in.active {
|
||||
background: url("../../../assets/images/zoomOutActive.png") no-repeat;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.zoom-out {
|
||||
background: url("../../../assets/images/zoomIn.png") no-repeat;
|
||||
@ -117,15 +350,43 @@ defineExpose({
|
||||
height: 40px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.zoom-out:hover,
|
||||
.zoom-out.active {
|
||||
background: url("../../../assets/images/zoomInActive.png") no-repeat;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 768px) {
|
||||
.zoom-in, .zoom-out, .compass {
|
||||
.zoom-in,
|
||||
.zoom-out,
|
||||
.compass-container,
|
||||
.compass-bg,
|
||||
.compass-needle {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.zoom-in,
|
||||
.zoom-out {
|
||||
background-size: 80%;
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.rotate-left,
|
||||
.rotate-right {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.rotate-left {
|
||||
left: -18px;
|
||||
}
|
||||
|
||||
.rotate-right {
|
||||
right: -18px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
677
src/components/map/controls/VehicleMovementControl copy.vue
Normal file
@ -0,0 +1,677 @@
|
||||
<template>
|
||||
<div class="vehicle-movement-control">
|
||||
<!-- 连接状态指示器 -->
|
||||
<div class="ws-status" :class="{ connected: wsConnected }">
|
||||
<span v-if="wsConnected">已连接</span>
|
||||
<span v-else>未连接</span>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<!-- 告警/预警提示框 -->
|
||||
<!-- <div class="alert-container" v-if="alertMessage"> -->
|
||||
<div class="alert-container">
|
||||
<div class="alert-box alert-flash" :class="{'alert-warning': alertType === 'warning', 'alert-danger': alertType === 'alarm'}">
|
||||
<div class="alert-row">
|
||||
<img v-if="alertType === 'alarm'" class="alert-icon" :src="alarmIcon" alt="alarm" />
|
||||
<img v-else-if="alertType === 'warning'" class="alert-icon" :src="warnIcon" alt="warning" />
|
||||
<span class="alert-title">
|
||||
{{ alertType === 'alarm' ? '冲突告警' : '冲突预警' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="alert-content alert-desc">
|
||||
{{ alertMessage }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue';
|
||||
import { Vector as VectorSource } from 'ol/source';
|
||||
import { Vector as VectorLayer } from 'ol/layer';
|
||||
import { Style, Icon } from 'ol/style';
|
||||
import Feature from 'ol/Feature';
|
||||
import Point from 'ol/geom/Point';
|
||||
import Overlay from 'ol/Overlay';
|
||||
import { transform } from 'ol/proj';
|
||||
// 导入WebSocketService
|
||||
import WebSocketService, { createWebSocket } from '../../../utils/websocket.js';
|
||||
|
||||
// 为SockJS提供polyfill
|
||||
if (typeof window !== 'undefined' && !window.global) {
|
||||
window.global = window;
|
||||
}
|
||||
|
||||
// 导入车辆图标
|
||||
import carIcon from '../../../assets/images/noPeopleCar.png';
|
||||
import aircraftIcon from '../../../assets/images/Aircraft.png';
|
||||
import aircraft1Icon from '../../../assets/images/Aircraft1.png';
|
||||
import labelBg from '../../../assets/images/label_bg.png';
|
||||
import airportBg from '../../../assets/images/airport_bg.png';
|
||||
import airportOutBg from '../../../assets/images/airport_out.png';
|
||||
import alarmBg from '../../../assets/images/alarm_bg.png';
|
||||
import warningBg from '../../../assets/images/warning_bg.png';
|
||||
import alarmIcon from '../../../assets/images/alarm_icon.png';
|
||||
import warnIcon from '../../../assets/images/warn_icon.png';
|
||||
// 定义props接收地图实例
|
||||
const props = defineProps({
|
||||
map: Object
|
||||
});
|
||||
|
||||
// WebSocket连接状态
|
||||
const wsConnected = ref(false);
|
||||
let wsService = null;
|
||||
|
||||
// 车辆数据存储
|
||||
const vehicles = ref({});
|
||||
let vehicleLayer = null;
|
||||
let vehicleSource = null;
|
||||
|
||||
// 告警/预警状态
|
||||
const alertMessage = ref('与航空器Y117距离小于600m!请及时避让');
|
||||
const alertType = ref('warning'); // 'warning' 或 'alarm'
|
||||
let alertTimer = null;
|
||||
|
||||
// 显示告警/预警消息
|
||||
function showAlert(message, type, duration = 5000) {
|
||||
// 清除可能存在的定时器
|
||||
if (alertTimer) {
|
||||
clearTimeout(alertTimer);
|
||||
}
|
||||
|
||||
// 设置消息和类型
|
||||
alertMessage.value = message;
|
||||
alertType.value = type; // 'warning' 或 'alarm'
|
||||
|
||||
// 设置自动消失
|
||||
alertTimer = setTimeout(() => {
|
||||
alertMessage.value = '';
|
||||
alertType.value = '';
|
||||
}, duration);
|
||||
}
|
||||
|
||||
// 创建车辆图层
|
||||
function createVehicleLayer() {
|
||||
if (!props.map) return;
|
||||
|
||||
// 创建矢量数据源
|
||||
vehicleSource = new VectorSource();
|
||||
|
||||
// 创建矢量图层
|
||||
vehicleLayer = new VectorLayer({
|
||||
source: vehicleSource,
|
||||
zIndex: 20, // 确保在其他图层之上
|
||||
});
|
||||
|
||||
// 添加图层到地图
|
||||
props.map.addLayer(vehicleLayer);
|
||||
console.log('车辆移动图层已创建');
|
||||
|
||||
// 设置地图监听器
|
||||
setupMapListeners();
|
||||
}
|
||||
|
||||
// 更新车辆位置
|
||||
function updateVehiclePosition(vehicleData) {
|
||||
if (!vehicleSource || !props.map) return;
|
||||
|
||||
const { object_id, object_type, position, heading, speed } = vehicleData;
|
||||
console.log(`接收到位置数据: ${object_id}`, position);
|
||||
|
||||
let coordinates;
|
||||
|
||||
|
||||
coordinates = transform(
|
||||
[position.longitude, position.latitude],
|
||||
'EPSG:4326',
|
||||
props.map.getView().getProjection()
|
||||
);
|
||||
|
||||
|
||||
// 检查该车辆是否已存在
|
||||
let feature = vehicleSource.getFeatureById(object_id);
|
||||
|
||||
// 根据车辆ID前缀和类型进行分类
|
||||
// 1. 判断航空器类型
|
||||
const isAircraftIn = object_type.toLowerCase().includes('aircraft') && !object_id.toLowerCase().includes('ac001');
|
||||
const isAircraftOut = object_id.toLowerCase().includes('ac001');
|
||||
const isAircraft = isAircraftIn || isAircraftOut;
|
||||
|
||||
// 2. 判断无人车类型
|
||||
const isUnmannedVehicle = object_type === 'UNMANNED_VEHICLE' || object_id.toLowerCase().startsWith('qn');
|
||||
|
||||
// 3. 判断特勤车类型
|
||||
const isSpecialVehicle = object_id.toLowerCase().startsWith('tq');
|
||||
|
||||
// 4. 判断摆渡车类型
|
||||
const isShuttleVehicle = object_id.toLowerCase().startsWith('bd');
|
||||
|
||||
// 选择图标 - 根据类型选择不同图标
|
||||
let iconSrc;
|
||||
if (isAircraftIn) {
|
||||
iconSrc = aircraftIcon; // 滑入航空器
|
||||
} else if (isAircraftOut) {
|
||||
iconSrc = aircraft1Icon; // 滑出航空器
|
||||
} else {
|
||||
iconSrc = carIcon; // 普通车辆
|
||||
}
|
||||
|
||||
// 创建样式
|
||||
const style = new Style({
|
||||
image: new Icon({
|
||||
src: iconSrc,
|
||||
scale: 1.5,
|
||||
anchor: [0.5, 0.5],
|
||||
rotation: ((heading - 72) * Math.PI) / 180,
|
||||
})
|
||||
});
|
||||
|
||||
if (!feature) {
|
||||
// 创建新的Feature
|
||||
feature = new Feature({
|
||||
geometry: new Point(coordinates),
|
||||
name: `${object_type} ${object_id}`,
|
||||
type: object_type,
|
||||
speed: speed,
|
||||
isAircraftIn: isAircraftIn,
|
||||
isAircraftOut: isAircraftOut,
|
||||
isAircraft: isAircraft,
|
||||
isUnmannedVehicle: isUnmannedVehicle,
|
||||
isSpecialVehicle: isSpecialVehicle,
|
||||
isShuttleVehicle: isShuttleVehicle
|
||||
});
|
||||
|
||||
feature.setId(object_id);
|
||||
feature.setStyle(style);
|
||||
vehicleSource.addFeature(feature);
|
||||
|
||||
// 存储车辆信息
|
||||
vehicles.value[object_id] = {
|
||||
id: object_id,
|
||||
type: object_type,
|
||||
position: coordinates,
|
||||
heading: heading,
|
||||
speed: speed,
|
||||
feature: feature,
|
||||
isAircraftIn: isAircraftIn,
|
||||
isAircraftOut: isAircraftOut,
|
||||
isAircraft: isAircraft,
|
||||
isUnmannedVehicle: isUnmannedVehicle,
|
||||
isSpecialVehicle: isSpecialVehicle,
|
||||
isShuttleVehicle: isShuttleVehicle
|
||||
};
|
||||
|
||||
// 创建标签 - 使用updateVehicleLabel来确保一致性
|
||||
updateVehicleLabel(object_id, coordinates, speed);
|
||||
} else {
|
||||
// 更新现有Feature
|
||||
feature.getGeometry().setCoordinates(coordinates);
|
||||
feature.setStyle(style);
|
||||
|
||||
// 更新存储的车辆信息
|
||||
vehicles.value[object_id] = {
|
||||
...vehicles.value[object_id],
|
||||
position: coordinates,
|
||||
heading: heading,
|
||||
speed: speed,
|
||||
isAircraftIn: isAircraftIn,
|
||||
isAircraftOut: isAircraftOut,
|
||||
isAircraft: isAircraft,
|
||||
isUnmannedVehicle: isUnmannedVehicle,
|
||||
isSpecialVehicle: isSpecialVehicle,
|
||||
isShuttleVehicle: isShuttleVehicle
|
||||
};
|
||||
|
||||
// 更新标签位置和内容
|
||||
updateVehicleLabel(object_id, coordinates, speed);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新车辆标签
|
||||
function updateVehicleLabel(id, position, speed) {
|
||||
if (!props.map || !vehicles.value[id]) return;
|
||||
|
||||
console.log(`更新标签位置: ${id}, 位置: [${position[0]}, ${position[1]}]`);
|
||||
|
||||
// 如果存在旧的overlay,先移除它
|
||||
if (vehicles.value[id].overlay) {
|
||||
props.map.removeOverlay(vehicles.value[id].overlay);
|
||||
}
|
||||
|
||||
// 获取车辆类型信息
|
||||
const vehicle = vehicles.value[id];
|
||||
const isAircraftIn = vehicle.isAircraftIn;
|
||||
const isAircraftOut = vehicle.isAircraftOut;
|
||||
const isUnmannedVehicle = vehicle.isUnmannedVehicle;
|
||||
const isSpecialVehicle = vehicle.isSpecialVehicle;
|
||||
const isShuttleVehicle = vehicle.isShuttleVehicle;
|
||||
const alarm = vehicle.alarm;
|
||||
const warning = vehicle.warning;
|
||||
|
||||
|
||||
// 选择背景图片 - 根据车辆类型选择不同背景
|
||||
let backgroundImage;
|
||||
if (isAircraftOut) {
|
||||
backgroundImage = airportOutBg; // 滑出航空器背景 - 黄色
|
||||
} else if (isAircraftIn) {
|
||||
backgroundImage = airportBg; // 滑入航空器背景 - 蓝色
|
||||
}else if(alarm){
|
||||
backgroundImage = alarmBg; // 告警背景
|
||||
} else if(warning){
|
||||
backgroundImage = warningBg; // 预警背景
|
||||
}else {
|
||||
backgroundImage = labelBg; // 普通车辆背景
|
||||
}
|
||||
|
||||
// 重新创建标签元素
|
||||
const labelDiv = document.createElement('div');
|
||||
labelDiv.className = `vehicle-label ${isAircraftIn ? 'vehicle-aircraft-in' : ''} ${isAircraftOut ? 'vehicle-aircraft-out' : ''} ${isUnmannedVehicle ? 'vehicle-unmanned' : ''} ${isSpecialVehicle ? 'vehicle-special' : ''} ${isShuttleVehicle ? 'vehicle-shuttle' : ''}`;
|
||||
|
||||
// 根据车辆类型设置不同的标签内容
|
||||
let labelText = '';
|
||||
labelText = `${id} ${speed.toFixed(2)} km/h`;
|
||||
labelDiv.innerHTML = labelText;
|
||||
labelDiv.style.backgroundImage = `url(${backgroundImage})`;
|
||||
labelDiv.style.backgroundSize = '100% 100%';
|
||||
labelDiv.style.color = '#fff';
|
||||
labelDiv.style.padding = '5px 10px';
|
||||
|
||||
// 创建新的Overlay
|
||||
const overlay = new Overlay({
|
||||
element: labelDiv,
|
||||
position: position,
|
||||
positioning: 'bottom-center', // 标签底部中心对准位置点
|
||||
offset: [0, -30], // 向上偏移,确保在图标上方
|
||||
stopEvent: false,
|
||||
insertFirst: true, // 确保在DOM中优先插入
|
||||
autoPan: false, // 禁用自动平移
|
||||
});
|
||||
|
||||
// 更新引用
|
||||
vehicles.value[id].overlay = overlay;
|
||||
vehicles.value[id].labelDiv = labelDiv;
|
||||
|
||||
// 添加到地图
|
||||
props.map.addOverlay(overlay);
|
||||
}
|
||||
|
||||
// 监听地图变化,确保标签位置正确更新
|
||||
function setupMapListeners() {
|
||||
if (!props.map) return;
|
||||
|
||||
// 监听地图移动结束事件
|
||||
props.map.on('moveend', () => {
|
||||
// 更新所有标签位置
|
||||
Object.keys(vehicles.value).forEach(id => {
|
||||
const vehicle = vehicles.value[id];
|
||||
if (vehicle.feature) {
|
||||
const coordinates = vehicle.feature.getGeometry().getCoordinates();
|
||||
updateVehicleLabel(id, coordinates, vehicle.speed);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 组件挂载时初始化
|
||||
onMounted(() => {
|
||||
// 创建图层并连接WebSocket
|
||||
if (props.map) {
|
||||
createVehicleLayer();
|
||||
}
|
||||
|
||||
// 连接WebSocket
|
||||
connectWebSocket();
|
||||
|
||||
// 设置定时发送心跳
|
||||
const pingInterval = setInterval(() => {
|
||||
if (wsService && wsConnected.value) {
|
||||
sendPing();
|
||||
}
|
||||
}, 30000); // 每30秒发送一次心跳
|
||||
|
||||
// 组件卸载时清理资源
|
||||
onUnmounted(() => {
|
||||
clearInterval(pingInterval);
|
||||
cleanup();
|
||||
});
|
||||
});
|
||||
|
||||
// 连接WebSocket
|
||||
function connectWebSocket() {
|
||||
try {
|
||||
// 使用WebSocketService创建连接
|
||||
const wsUrl = 'ws://10.0.0.124:8080/collision';
|
||||
console.log(`正在连接WebSocket: ${wsUrl}`);
|
||||
|
||||
wsService = createWebSocket(wsUrl, {
|
||||
reconnectInterval: 3000,
|
||||
maxReconnectAttempts: 5
|
||||
});
|
||||
|
||||
// 注册事件监听
|
||||
wsService.on('open', (event) => {
|
||||
console.log('WebSocket连接成功!');
|
||||
wsConnected.value = true;
|
||||
|
||||
// 连接成功后自动订阅消息
|
||||
setTimeout(() => {
|
||||
sendSubscribe();
|
||||
}, 1000); // 延迟1秒发送订阅,确保连接稳定
|
||||
});
|
||||
|
||||
wsService.on('message', (data) => {
|
||||
handleWsMessage(data);
|
||||
});
|
||||
|
||||
wsService.on('error', (event) => {
|
||||
console.error('WebSocket错误:', event);
|
||||
wsConnected.value = false;
|
||||
});
|
||||
|
||||
wsService.on('close', (event) => {
|
||||
console.log(`WebSocket连接关闭: ${event.code} - ${event.reason}`);
|
||||
wsConnected.value = false;
|
||||
});
|
||||
|
||||
wsService.on('reconnect_failed', () => {
|
||||
console.error('WebSocket重连失败,已达到最大重试次数');
|
||||
wsConnected.value = false;
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('创建WebSocket连接失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 处理WebSocket消息
|
||||
function handleWsMessage(message) {
|
||||
try {
|
||||
const data = JSON.parse(message);
|
||||
|
||||
// 根据消息类型处理
|
||||
switch (data.type) {
|
||||
case 'connection':
|
||||
console.log(`连接确认: ${data.message}`);
|
||||
break;
|
||||
case 'position_update':
|
||||
// 确保payload存在
|
||||
if (data.payload && data.payload.object_id) {
|
||||
updateVehiclePosition(data.payload);
|
||||
} else {
|
||||
console.error('位置更新消息格式错误:', data);
|
||||
}
|
||||
break;
|
||||
case 'pong':
|
||||
console.log('收到心跳响应');
|
||||
break;
|
||||
case 'collision_warning':
|
||||
console.log('收到碰撞预警:', data.payload);
|
||||
// 显示预警信息
|
||||
if (data.payload) {
|
||||
// 获取车辆ID和预警信息
|
||||
const vehicleId = data.payload.object_id || '未知车辆';
|
||||
const distance = data.payload.distance || 0;
|
||||
const message = `预警:${vehicleId} 与其他车辆距离${distance.toFixed(1)}米,请注意避让!`;
|
||||
showAlert(message, 'warning', 8000);
|
||||
|
||||
// 如果需要在地图上标记该车辆的告警状态
|
||||
if (vehicles.value[vehicleId]) {
|
||||
vehicles.value[vehicleId].warning = true;
|
||||
// 如果车辆已有位置信息,更新标签显示
|
||||
if (vehicles.value[vehicleId].position) {
|
||||
updateVehicleLabel(vehicleId, vehicles.value[vehicleId].position, vehicles.value[vehicleId].speed);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'rule_violation':
|
||||
console.log('收到规则违规:', data.payload);
|
||||
// 显示告警信息
|
||||
if (data.payload) {
|
||||
// 获取车辆ID和告警信息
|
||||
const vehicleId = data.payload.object_id || '未知车辆';
|
||||
const violationType = data.payload.violation_type || '未知违规';
|
||||
const message = `告警:${vehicleId} 发生${violationType},请立即处理!`;
|
||||
showAlert(message, 'alarm', 10000);
|
||||
|
||||
// 如果需要在地图上标记该车辆的告警状态
|
||||
if (vehicles.value[vehicleId]) {
|
||||
vehicles.value[vehicleId].alarm = true;
|
||||
// 如果车辆已有位置信息,更新标签显示
|
||||
if (vehicles.value[vehicleId].position) {
|
||||
updateVehicleLabel(vehicleId, vehicles.value[vehicleId].position, vehicles.value[vehicleId].speed);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'vehicle_command':
|
||||
console.log('收到车辆控制指令:', data.payload);
|
||||
break;
|
||||
default:
|
||||
// 其他类型的消息可以根据需要处理
|
||||
console.log(`收到其他类型消息: ${data.type}`, data);
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('处理WebSocket消息出错:', e, message);
|
||||
}
|
||||
}
|
||||
|
||||
// 发送心跳
|
||||
function sendPing() {
|
||||
if (wsService) {
|
||||
wsService.send('ping');
|
||||
console.log('发送心跳: ping');
|
||||
}
|
||||
}
|
||||
|
||||
// 订阅消息
|
||||
function sendSubscribe() {
|
||||
if (wsService) {
|
||||
const message = JSON.stringify({
|
||||
type: 'subscribe',
|
||||
topics: ['position_update', 'collision_warning', 'rule_violation'],
|
||||
timestamp: Date.now()
|
||||
});
|
||||
wsService.send(message);
|
||||
console.log('发送订阅请求');
|
||||
}
|
||||
}
|
||||
|
||||
// 清理资源
|
||||
function cleanup() {
|
||||
// 断开WebSocket连接
|
||||
if (wsService) {
|
||||
wsService.close();
|
||||
wsService = null;
|
||||
}
|
||||
|
||||
// 移除图层
|
||||
if (vehicleLayer && props.map) {
|
||||
props.map.removeLayer(vehicleLayer);
|
||||
vehicleLayer = null;
|
||||
}
|
||||
|
||||
// 移除标签
|
||||
if (props.map) {
|
||||
Object.values(vehicles.value).forEach(vehicle => {
|
||||
if (vehicle.overlay) {
|
||||
console.log(`移除标签: ${vehicle.id}`);
|
||||
props.map.removeOverlay(vehicle.overlay);
|
||||
vehicle.overlay = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 清空车辆数据
|
||||
vehicles.value = {};
|
||||
}
|
||||
|
||||
// 监听地图实例变化
|
||||
watch(() => props.map, (newMap) => {
|
||||
if (newMap) {
|
||||
createVehicleLayer();
|
||||
}
|
||||
});
|
||||
|
||||
// 向外暴露方法
|
||||
defineExpose({
|
||||
updateVehiclePosition,
|
||||
wsConnected,
|
||||
sendPing,
|
||||
sendSubscribe
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.vehicle-movement-control {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.ws-status {
|
||||
padding: 5px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
background-color: rgba(255, 87, 34, 0.8);
|
||||
color: white;
|
||||
transition: background-color 0.3s ease;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ws-status.connected {
|
||||
background-color: rgba(76, 175, 80, 0.8);
|
||||
}
|
||||
|
||||
/* 告警/预警容器样式 */
|
||||
.alert-container {
|
||||
position: fixed;
|
||||
top: 20%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 2000;
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
max-width: 550px;
|
||||
text-align: center;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.alert-box {
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content:center;
|
||||
align-items: center;
|
||||
animation: alertSlideIn 1s ease;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.alert-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
/* margin-bottom: 8px; */
|
||||
}
|
||||
|
||||
.alert-icon {
|
||||
width: 56px;
|
||||
height: 50px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.alert-title {
|
||||
font-size: 26px;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.alert-desc {
|
||||
font-size: 18px;
|
||||
color: #fff;
|
||||
margin-left: 44px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* 闪烁动画 */
|
||||
.alert-flash {
|
||||
animation: alertFlash 1s steps(2, start) infinite, alertSlideIn 0.5s ease;
|
||||
}
|
||||
@keyframes alertFlash {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.4; }
|
||||
}
|
||||
|
||||
/* 预警样式 - 黄色背景 */
|
||||
.alert-warning {
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
max-width: 550px;
|
||||
background: url(../../../assets/images/warn_report.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 告警样式 - 红色背景 */
|
||||
.alert-danger {
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
max-width: 550px;
|
||||
background: url(../../../assets/images/alarm_report.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 车辆标签样式 */
|
||||
:deep(.vehicle-label) {
|
||||
position: absolute;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
transform: translateX(-50%);
|
||||
text-align: center;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
min-width: 80px;
|
||||
min-height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 30; /* 确保标签在图标上方 */
|
||||
transition: transform 0.3s ease, left 0.3s ease, top 0.3s ease; /* 平滑移动效果 */
|
||||
will-change: transform, left, top; /* 提示浏览器优化这些属性的变化 */
|
||||
bottom: 0; /* 确保标签底部对齐定位点 */
|
||||
}
|
||||
|
||||
/* 滑入航空器标签样式 */
|
||||
:deep(.vehicle-aircraft-in) {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 滑出航空器标签样式 */
|
||||
:deep(.vehicle-aircraft-out) {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 无人车标签样式 */
|
||||
:deep(.vehicle-unmanned) {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 特勤车标签样式 */
|
||||
:deep(.vehicle-special) {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 摆渡车标签样式 */
|
||||
:deep(.vehicle-shuttle) {
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
750
src/components/map/controls/VehicleMovementControl.vue
Normal file
@ -0,0 +1,750 @@
|
||||
<template>
|
||||
|
||||
<!-- 告警/预警提示框 -->
|
||||
<!-- <div class="alert-container" v-if="alertMessage"> -->
|
||||
<div class="alert-container">
|
||||
<div class="alert-box alert-flash" :class="{'alert-warning': alertType === 'warning', 'alert-danger': alertType === 'alarm'}">
|
||||
<div class="alert-row">
|
||||
<img v-if="alertType === 'alarm'" class="alert-icon" :src="alarmIcon" alt="alarm" />
|
||||
<img v-else-if="alertType === 'warning'" class="alert-icon" :src="warnIcon" alt="warning" />
|
||||
<span class="alert-title">
|
||||
{{ alertType === 'alarm' ? '冲突告警' : '冲突预警' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="alert-content alert-desc">
|
||||
{{ alertMessage }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue';
|
||||
import { Vector as VectorSource } from 'ol/source';
|
||||
import { Vector as VectorLayer } from 'ol/layer';
|
||||
import { Style, Icon } from 'ol/style';
|
||||
import Feature from 'ol/Feature';
|
||||
import Point from 'ol/geom/Point';
|
||||
import Overlay from 'ol/Overlay';
|
||||
import { transform } from 'ol/proj';
|
||||
// 导入WebSocketService
|
||||
import WebSocketService, { createWebSocket } from '../../../utils/websocket.js';
|
||||
|
||||
// 为SockJS提供polyfill
|
||||
if (typeof window !== 'undefined' && !window.global) {
|
||||
window.global = window;
|
||||
}
|
||||
|
||||
// 导入车辆图标
|
||||
import carIcon from '../../../assets/images/noPeopleCar.png';
|
||||
import aircraftIcon from '../../../assets/images/Aircraft.png';
|
||||
import aircraft1Icon from '../../../assets/images/Aircraft1.png';
|
||||
import labelBg from '../../../assets/images/label_bg.png';
|
||||
import airportBg from '../../../assets/images/airport_bg.png';
|
||||
import airportOutBg from '../../../assets/images/airport_out.png';
|
||||
import alarmBg from '../../../assets/images/alarm_bg.png';
|
||||
import warningBg from '../../../assets/images/warning_bg.png';
|
||||
import alarmIcon from '../../../assets/images/alarm_icon.png';
|
||||
import warnIcon from '../../../assets/images/warn_icon.png';
|
||||
// 定义props接收地图实例
|
||||
const props = defineProps({
|
||||
map: Object
|
||||
});
|
||||
|
||||
// WebSocket连接状态
|
||||
const wsConnected = ref(false);
|
||||
let wsService = null;
|
||||
|
||||
// 车辆数据存储
|
||||
const vehicles = ref({});
|
||||
let vehicleLayer = null;
|
||||
let vehicleSource = null;
|
||||
|
||||
// 动态收集所有车辆/航空器类别
|
||||
const vehicleCategories = ref({});
|
||||
|
||||
// 告警/预警状态
|
||||
const alertMessage = ref('与航空器Y117距离小于600m!请及时避让');
|
||||
const alertType = ref('warning'); // 'warning' 或 'alarm'
|
||||
let alertTimer = null;
|
||||
|
||||
// 显示告警/预警消息
|
||||
function showAlert(message, type, duration = 5000) {
|
||||
// 清除可能存在的定时器
|
||||
if (alertTimer) {
|
||||
clearTimeout(alertTimer);
|
||||
}
|
||||
|
||||
// 设置消息和类型
|
||||
alertMessage.value = message;
|
||||
alertType.value = type; // 'warning' 或 'alarm'
|
||||
|
||||
// 设置自动消失
|
||||
alertTimer = setTimeout(() => {
|
||||
alertMessage.value = '';
|
||||
alertType.value = '';
|
||||
}, duration);
|
||||
}
|
||||
|
||||
// 创建车辆图层
|
||||
function createVehicleLayer() {
|
||||
if (!props.map) return;
|
||||
|
||||
// 创建矢量数据源
|
||||
vehicleSource = new VectorSource();
|
||||
|
||||
// 创建矢量图层
|
||||
vehicleLayer = new VectorLayer({
|
||||
source: vehicleSource,
|
||||
zIndex: 20, // 确保在其他图层之上
|
||||
});
|
||||
|
||||
// 添加图层到地图
|
||||
props.map.addLayer(vehicleLayer);
|
||||
console.log('车辆移动图层已创建');
|
||||
|
||||
// 设置地图监听器
|
||||
setupMapListeners();
|
||||
}
|
||||
|
||||
// 更新车辆位置
|
||||
function updateVehiclePosition(vehicleData) {
|
||||
if (!vehicleSource || !props.map) return;
|
||||
|
||||
const { object_id, object_type, position, heading, speed } = vehicleData;
|
||||
console.log(`接收到位置数据: ${object_id}`, position);
|
||||
|
||||
let coordinates;
|
||||
|
||||
|
||||
coordinates = transform(
|
||||
[position.longitude, position.latitude],
|
||||
'EPSG:4326',
|
||||
props.map.getView().getProjection()
|
||||
);
|
||||
|
||||
|
||||
// 检查该车辆是否已存在
|
||||
let feature = vehicleSource.getFeatureById(object_id);
|
||||
|
||||
// 根据车辆ID前缀和类型进行分类
|
||||
// 1. 判断航空器类型
|
||||
const isAircraftIn = object_type.toLowerCase().includes('aircraft') && !object_id.toLowerCase().includes('ac001');
|
||||
const isAircraftOut = object_id.toLowerCase().includes('ac001');
|
||||
const isAircraft = isAircraftIn || isAircraftOut;
|
||||
|
||||
// 2. 判断无人车类型
|
||||
const isUnmannedVehicle = object_type === 'UNMANNED_VEHICLE' || object_id.toLowerCase().startsWith('qn');
|
||||
|
||||
// 3. 判断特勤车类型
|
||||
const isSpecialVehicle = object_id.toLowerCase().startsWith('tq');
|
||||
|
||||
// 4. 判断摆渡车类型
|
||||
const isShuttleVehicle = object_id.toLowerCase().startsWith('bd');
|
||||
|
||||
// 选择图标 - 根据类型选择不同图标
|
||||
let iconSrc;
|
||||
if (isAircraftIn) {
|
||||
iconSrc = aircraftIcon; // 滑入航空器
|
||||
} else if (isAircraftOut) {
|
||||
iconSrc = aircraft1Icon; // 滑出航空器
|
||||
} else {
|
||||
iconSrc = carIcon; // 普通车辆
|
||||
}
|
||||
|
||||
// 动态收集类别
|
||||
let typeKey = object_type; // 默认使用对象类型作为键
|
||||
|
||||
// 根据车辆特性进一步细化分类
|
||||
if (isAircraftIn) {
|
||||
typeKey = 'AIRCRAFT_IN';
|
||||
} else if (isAircraftOut) {
|
||||
typeKey = 'AIRCRAFT_OUT';
|
||||
} else if (isUnmannedVehicle) {
|
||||
typeKey = 'UNMANNED_VEHICLE';
|
||||
} else if (isSpecialVehicle) {
|
||||
typeKey = 'SPECIAL_VEHICLE';
|
||||
} else if (isShuttleVehicle) {
|
||||
typeKey = 'SHUTTLE_VEHICLE';
|
||||
}
|
||||
|
||||
// 根据typeKey收集分类
|
||||
if (!vehicleCategories.value[typeKey]) {
|
||||
vehicleCategories.value[typeKey] = {
|
||||
name: isAircraftIn ? '滑入航空器' :
|
||||
isAircraftOut ? '滑出航空器' :
|
||||
isUnmannedVehicle ? '无人车' :
|
||||
isSpecialVehicle ? '特勤车' :
|
||||
isShuttleVehicle ? '摆渡车' : object_type,
|
||||
icon: iconSrc,
|
||||
visible: true,
|
||||
showLabel: true
|
||||
};
|
||||
}
|
||||
|
||||
// 创建样式 - 只有当类别设置为可见时才显示图标
|
||||
const style = vehicleCategories.value[typeKey].visible ?
|
||||
new Style({
|
||||
image: new Icon({
|
||||
src: iconSrc,
|
||||
scale: 1.5,
|
||||
anchor: [0.5, 0.5],
|
||||
rotation: ((heading - 72) * Math.PI) / 180,
|
||||
})
|
||||
}) : new Style({}); // 使用空样式替代null,完全隐藏图标
|
||||
|
||||
if (!feature) {
|
||||
// 创建新的Feature
|
||||
feature = new Feature({
|
||||
geometry: new Point(coordinates),
|
||||
name: `${object_type} ${object_id}`,
|
||||
type: object_type,
|
||||
speed: speed,
|
||||
isAircraftIn: isAircraftIn,
|
||||
isAircraftOut: isAircraftOut,
|
||||
isAircraft: isAircraft,
|
||||
isUnmannedVehicle: isUnmannedVehicle,
|
||||
isSpecialVehicle: isSpecialVehicle,
|
||||
isShuttleVehicle: isShuttleVehicle
|
||||
});
|
||||
|
||||
feature.setId(object_id);
|
||||
feature.setStyle(style);
|
||||
vehicleSource.addFeature(feature);
|
||||
|
||||
// 存储车辆信息
|
||||
vehicles.value[object_id] = {
|
||||
id: object_id,
|
||||
type: object_type,
|
||||
position: coordinates,
|
||||
heading: heading,
|
||||
speed: speed,
|
||||
feature: feature,
|
||||
isAircraftIn: isAircraftIn,
|
||||
isAircraftOut: isAircraftOut,
|
||||
isAircraft: isAircraft,
|
||||
isUnmannedVehicle: isUnmannedVehicle,
|
||||
isSpecialVehicle: isSpecialVehicle,
|
||||
isShuttleVehicle: isShuttleVehicle
|
||||
};
|
||||
|
||||
// 只有当设置显示标签时才创建标签
|
||||
if (vehicleCategories.value[typeKey].showLabel) {
|
||||
updateVehicleLabel(object_id, coordinates, speed);
|
||||
}
|
||||
} else {
|
||||
// 更新现有Feature
|
||||
feature.getGeometry().setCoordinates(coordinates);
|
||||
feature.setStyle(style); // 应用最新的样式,可能是null(隐藏)
|
||||
|
||||
// 更新存储的车辆信息
|
||||
vehicles.value[object_id] = {
|
||||
...vehicles.value[object_id],
|
||||
position: coordinates,
|
||||
heading: heading,
|
||||
speed: speed,
|
||||
isAircraftIn: isAircraftIn,
|
||||
isAircraftOut: isAircraftOut,
|
||||
isAircraft: isAircraft,
|
||||
isUnmannedVehicle: isUnmannedVehicle,
|
||||
isSpecialVehicle: isSpecialVehicle,
|
||||
isShuttleVehicle: isShuttleVehicle
|
||||
};
|
||||
|
||||
// 只有当设置显示标签时才更新标签
|
||||
if (vehicleCategories.value[typeKey].showLabel) {
|
||||
updateVehicleLabel(object_id, coordinates, speed);
|
||||
} else if (vehicles.value[object_id].overlay) {
|
||||
// 如果不显示标签但存在overlay,需要移除它
|
||||
try {
|
||||
props.map.removeOverlay(vehicles.value[object_id].overlay);
|
||||
} catch(e) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新车辆标签
|
||||
function updateVehicleLabel(id, position, speed) {
|
||||
if (!props.map || !vehicles.value[id]) return;
|
||||
|
||||
console.log(`更新标签位置: ${id}, 位置: [${position[0]}, ${position[1]}]`);
|
||||
|
||||
// 如果存在旧的overlay,先移除它
|
||||
if (vehicles.value[id].overlay) {
|
||||
props.map.removeOverlay(vehicles.value[id].overlay);
|
||||
}
|
||||
|
||||
// 获取车辆类型信息
|
||||
const vehicle = vehicles.value[id];
|
||||
const isAircraftIn = vehicle.isAircraftIn;
|
||||
const isAircraftOut = vehicle.isAircraftOut;
|
||||
const isUnmannedVehicle = vehicle.isUnmannedVehicle;
|
||||
const isSpecialVehicle = vehicle.isSpecialVehicle;
|
||||
const isShuttleVehicle = vehicle.isShuttleVehicle;
|
||||
const alarm = vehicle.alarm;
|
||||
const warning = vehicle.warning;
|
||||
|
||||
// 确定车辆类别的key
|
||||
let typeKey = vehicle.type;
|
||||
if (isAircraftIn) typeKey = 'AIRCRAFT_IN';
|
||||
else if (isAircraftOut) typeKey = 'AIRCRAFT_OUT';
|
||||
else if (isUnmannedVehicle) typeKey = 'UNMANNED_VEHICLE';
|
||||
else if (isSpecialVehicle) typeKey = 'SPECIAL_VEHICLE';
|
||||
else if (isShuttleVehicle) typeKey = 'SHUTTLE_VEHICLE';
|
||||
|
||||
// 选择背景图片 - 根据车辆类型选择不同背景
|
||||
let backgroundImage;
|
||||
if (isAircraftOut) {
|
||||
backgroundImage = airportOutBg; // 滑出航空器背景 - 黄色
|
||||
} else if (isAircraftIn) {
|
||||
backgroundImage = airportBg; // 滑入航空器背景 - 蓝色
|
||||
}else if(alarm){
|
||||
backgroundImage = alarmBg; // 告警背景
|
||||
} else if(warning){
|
||||
backgroundImage = warningBg; // 预警背景
|
||||
}else {
|
||||
backgroundImage = labelBg; // 普通车辆背景
|
||||
}
|
||||
|
||||
// 重新创建标签元素
|
||||
const labelDiv = document.createElement('div');
|
||||
labelDiv.className = `vehicle-label ${isAircraftIn ? 'vehicle-aircraft-in' : ''} ${isAircraftOut ? 'vehicle-aircraft-out' : ''} ${isUnmannedVehicle ? 'vehicle-unmanned' : ''} ${isSpecialVehicle ? 'vehicle-special' : ''} ${isShuttleVehicle ? 'vehicle-shuttle' : ''}`;
|
||||
|
||||
// 根据车辆类型设置不同的标签内容
|
||||
let labelText = '';
|
||||
labelText = `${id} ${speed.toFixed(2)} km/h`;
|
||||
labelDiv.innerHTML = labelText;
|
||||
labelDiv.style.backgroundImage = `url(${backgroundImage})`;
|
||||
labelDiv.style.backgroundSize = '100% 100%';
|
||||
labelDiv.style.color = '#fff';
|
||||
labelDiv.style.padding = '5px 10px';
|
||||
|
||||
// 创建新的Overlay
|
||||
const overlay = new Overlay({
|
||||
element: labelDiv,
|
||||
position: position,
|
||||
positioning: 'bottom-center', // 标签底部中心对准位置点
|
||||
offset: [0, -30], // 向上偏移,确保在图标上方
|
||||
stopEvent: false,
|
||||
insertFirst: true, // 确保在DOM中优先插入
|
||||
autoPan: false, // 禁用自动平移
|
||||
});
|
||||
|
||||
// 更新引用
|
||||
vehicles.value[id].overlay = overlay;
|
||||
vehicles.value[id].labelDiv = labelDiv;
|
||||
|
||||
// 添加到地图 - 不再需要额外检查图标是否可见
|
||||
props.map.addOverlay(overlay);
|
||||
}
|
||||
|
||||
// 监听地图变化,确保标签位置正确更新
|
||||
function setupMapListeners() {
|
||||
if (!props.map) return;
|
||||
|
||||
// 监听地图移动结束事件
|
||||
props.map.on('moveend', () => {
|
||||
// 更新所有标签位置
|
||||
Object.keys(vehicles.value).forEach(id => {
|
||||
const vehicle = vehicles.value[id];
|
||||
if (vehicle.feature) {
|
||||
const coordinates = vehicle.feature.getGeometry().getCoordinates();
|
||||
updateVehicleLabel(id, coordinates, vehicle.speed);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 组件挂载时初始化
|
||||
onMounted(() => {
|
||||
// 创建图层并连接WebSocket
|
||||
if (props.map) {
|
||||
createVehicleLayer();
|
||||
}
|
||||
|
||||
// 连接WebSocket
|
||||
connectWebSocket();
|
||||
|
||||
// 设置定时发送心跳
|
||||
const pingInterval = setInterval(() => {
|
||||
if (wsService && wsConnected.value) {
|
||||
sendPing();
|
||||
}
|
||||
}, 30000); // 每30秒发送一次心跳
|
||||
|
||||
// 组件卸载时清理资源
|
||||
onUnmounted(() => {
|
||||
clearInterval(pingInterval);
|
||||
cleanup();
|
||||
});
|
||||
});
|
||||
|
||||
// 连接WebSocket
|
||||
function connectWebSocket() {
|
||||
try {
|
||||
// 使用WebSocketService创建连接
|
||||
const wsUrl = 'ws://10.0.0.124:8080/collision';
|
||||
console.log(`正在连接WebSocket: ${wsUrl}`);
|
||||
|
||||
wsService = createWebSocket(wsUrl, {
|
||||
reconnectInterval: 3000,
|
||||
maxReconnectAttempts: 5
|
||||
});
|
||||
|
||||
// 注册事件监听
|
||||
wsService.on('open', (event) => {
|
||||
console.log('WebSocket连接成功!');
|
||||
wsConnected.value = true;
|
||||
|
||||
// 连接成功后自动订阅消息
|
||||
setTimeout(() => {
|
||||
sendSubscribe();
|
||||
}, 1000); // 延迟1秒发送订阅,确保连接稳定
|
||||
});
|
||||
|
||||
wsService.on('message', (data) => {
|
||||
handleWsMessage(data);
|
||||
});
|
||||
|
||||
wsService.on('error', (event) => {
|
||||
console.error('WebSocket错误:', event);
|
||||
wsConnected.value = false;
|
||||
});
|
||||
|
||||
wsService.on('close', (event) => {
|
||||
console.log(`WebSocket连接关闭: ${event.code} - ${event.reason}`);
|
||||
wsConnected.value = false;
|
||||
});
|
||||
|
||||
wsService.on('reconnect_failed', () => {
|
||||
console.error('WebSocket重连失败,已达到最大重试次数');
|
||||
wsConnected.value = false;
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('创建WebSocket连接失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 处理WebSocket消息
|
||||
function handleWsMessage(message) {
|
||||
try {
|
||||
const data = JSON.parse(message);
|
||||
|
||||
// 根据消息类型处理
|
||||
switch (data.type) {
|
||||
case 'connection':
|
||||
console.log(`连接确认: ${data.message}`);
|
||||
break;
|
||||
case 'position_update':
|
||||
// 确保payload存在
|
||||
if (data.payload && data.payload.object_id) {
|
||||
updateVehiclePosition(data.payload);
|
||||
} else {
|
||||
console.error('位置更新消息格式错误:', data);
|
||||
}
|
||||
break;
|
||||
case 'pong':
|
||||
console.log('收到心跳响应');
|
||||
break;
|
||||
case 'collision_warning':
|
||||
console.log('收到碰撞预警:', data.payload);
|
||||
// 显示预警信息
|
||||
if (data.payload) {
|
||||
// 获取车辆ID和预警信息
|
||||
const vehicleId = data.payload.object_id || '未知车辆';
|
||||
const distance = data.payload.distance || 0;
|
||||
const message = `预警:${vehicleId} 与其他车辆距离${distance.toFixed(1)}米,请注意避让!`;
|
||||
showAlert(message, 'warning', 8000);
|
||||
|
||||
// 如果需要在地图上标记该车辆的告警状态
|
||||
if (vehicles.value[vehicleId]) {
|
||||
vehicles.value[vehicleId].warning = true;
|
||||
// 如果车辆已有位置信息,更新标签显示
|
||||
if (vehicles.value[vehicleId].position) {
|
||||
updateVehicleLabel(vehicleId, vehicles.value[vehicleId].position, vehicles.value[vehicleId].speed);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'rule_violation':
|
||||
console.log('收到规则违规:', data.payload);
|
||||
// 显示告警信息
|
||||
if (data.payload) {
|
||||
// 获取车辆ID和告警信息
|
||||
const vehicleId = data.payload.object_id || '未知车辆';
|
||||
const violationType = data.payload.violation_type || '未知违规';
|
||||
const message = `告警:${vehicleId} 发生${violationType},请立即处理!`;
|
||||
showAlert(message, 'alarm', 10000);
|
||||
|
||||
// 如果需要在地图上标记该车辆的告警状态
|
||||
if (vehicles.value[vehicleId]) {
|
||||
vehicles.value[vehicleId].alarm = true;
|
||||
// 如果车辆已有位置信息,更新标签显示
|
||||
if (vehicles.value[vehicleId].position) {
|
||||
updateVehicleLabel(vehicleId, vehicles.value[vehicleId].position, vehicles.value[vehicleId].speed);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'vehicle_command':
|
||||
console.log('收到车辆控制指令:', data.payload);
|
||||
break;
|
||||
default:
|
||||
// 其他类型的消息可以根据需要处理
|
||||
console.log(`收到其他类型消息: ${data.type}`, data);
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('处理WebSocket消息出错:', e, message);
|
||||
}
|
||||
}
|
||||
|
||||
// 发送心跳
|
||||
function sendPing() {
|
||||
if (wsService) {
|
||||
wsService.send('ping');
|
||||
console.log('发送心跳: ping');
|
||||
}
|
||||
}
|
||||
|
||||
// 订阅消息
|
||||
function sendSubscribe() {
|
||||
if (wsService) {
|
||||
const message = JSON.stringify({
|
||||
type: 'subscribe',
|
||||
topics: ['position_update', 'collision_warning', 'rule_violation'],
|
||||
timestamp: Date.now()
|
||||
});
|
||||
wsService.send(message);
|
||||
console.log('发送订阅请求');
|
||||
}
|
||||
}
|
||||
|
||||
// 清理资源
|
||||
function cleanup() {
|
||||
// 断开WebSocket连接
|
||||
if (wsService) {
|
||||
wsService.close();
|
||||
wsService = null;
|
||||
}
|
||||
|
||||
// 移除图层
|
||||
if (vehicleLayer && props.map) {
|
||||
props.map.removeLayer(vehicleLayer);
|
||||
vehicleLayer = null;
|
||||
}
|
||||
|
||||
// 移除标签
|
||||
if (props.map) {
|
||||
Object.values(vehicles.value).forEach(vehicle => {
|
||||
if (vehicle.overlay) {
|
||||
console.log(`移除标签: ${vehicle.id}`);
|
||||
props.map.removeOverlay(vehicle.overlay);
|
||||
vehicle.overlay = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 清空车辆数据
|
||||
vehicles.value = {};
|
||||
}
|
||||
|
||||
// 监听地图实例变化
|
||||
watch(() => props.map, (newMap) => {
|
||||
if (newMap) {
|
||||
createVehicleLayer();
|
||||
}
|
||||
});
|
||||
|
||||
// 向外暴露方法
|
||||
defineExpose({
|
||||
updateVehiclePosition,
|
||||
wsConnected,
|
||||
sendPing,
|
||||
sendSubscribe,
|
||||
vehicleCategories,
|
||||
setCategoryVisibility(type, { visible, showLabel }) {
|
||||
if (vehicleCategories.value[type]) {
|
||||
// 更新分类设置
|
||||
vehicleCategories.value[type].visible = visible;
|
||||
vehicleCategories.value[type].showLabel = showLabel;
|
||||
|
||||
// 立即应用更改 - 更新所有属于此类别的车辆
|
||||
Object.values(vehicles.value).forEach(vehicle => {
|
||||
let vehicleTypeKey = vehicle.type;
|
||||
if (vehicle.isAircraftIn) vehicleTypeKey = 'AIRCRAFT_IN';
|
||||
else if (vehicle.isAircraftOut) vehicleTypeKey = 'AIRCRAFT_OUT';
|
||||
else if (vehicle.isUnmannedVehicle) vehicleTypeKey = 'UNMANNED_VEHICLE';
|
||||
else if (vehicle.isSpecialVehicle) vehicleTypeKey = 'SPECIAL_VEHICLE';
|
||||
else if (vehicle.isShuttleVehicle) vehicleTypeKey = 'SHUTTLE_VEHICLE';
|
||||
|
||||
// 如果车辆属于当前修改的类别
|
||||
if (vehicleTypeKey === type) {
|
||||
// 更新图标可见性 - 如果不可见,则完全移除图标样式
|
||||
if (vehicle.feature) {
|
||||
if (visible) {
|
||||
// 显示图标
|
||||
vehicle.feature.setStyle(
|
||||
new Style({
|
||||
image: new Icon({
|
||||
src: vehicleCategories.value[type].icon,
|
||||
scale: 1.5,
|
||||
anchor: [0.5, 0.5],
|
||||
rotation: ((vehicle.heading - 72) * Math.PI) / 180,
|
||||
})
|
||||
})
|
||||
);
|
||||
} else {
|
||||
// 完全隐藏图标,使用空样式而非null
|
||||
vehicle.feature.setStyle(new Style({}));
|
||||
}
|
||||
}
|
||||
|
||||
// 单独控制文本标签可见性,与图标显示状态无关
|
||||
if (vehicle.overlay) {
|
||||
if (showLabel) {
|
||||
try { props.map.removeOverlay(vehicle.overlay); } catch (e) {}
|
||||
props.map.addOverlay(vehicle.overlay);
|
||||
} else {
|
||||
try { props.map.removeOverlay(vehicle.overlay); } catch (e) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
/* 告警/预警容器样式 */
|
||||
.alert-container {
|
||||
position: fixed;
|
||||
top: 20%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 2000;
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
max-width: 550px;
|
||||
text-align: center;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.alert-box {
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content:center;
|
||||
align-items: center;
|
||||
animation: alertSlideIn 1s ease;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.alert-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
/* margin-bottom: 8px; */
|
||||
}
|
||||
|
||||
.alert-icon {
|
||||
width: 56px;
|
||||
height: 50px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.alert-title {
|
||||
font-size: 26px;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.alert-desc {
|
||||
font-size: 18px;
|
||||
color: #fff;
|
||||
margin-left: 44px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* 闪烁动画 */
|
||||
.alert-flash {
|
||||
animation: alertFlash 1s steps(2, start) infinite, alertSlideIn 0.5s ease;
|
||||
}
|
||||
@keyframes alertFlash {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.4; }
|
||||
}
|
||||
|
||||
/* 预警样式 - 黄色背景 */
|
||||
.alert-warning {
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
max-width: 550px;
|
||||
background: url(../../../assets/images/warn_report.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 告警样式 - 红色背景 */
|
||||
.alert-danger {
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
max-width: 550px;
|
||||
background: url(../../../assets/images/alarm_report.png) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 车辆标签样式 */
|
||||
:deep(.vehicle-label) {
|
||||
position: absolute;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
transform: translateX(-50%);
|
||||
text-align: center;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
min-width: 80px;
|
||||
min-height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 30; /* 确保标签在图标上方 */
|
||||
transition: transform 0.3s ease, left 0.3s ease, top 0.3s ease; /* 平滑移动效果 */
|
||||
will-change: transform, left, top; /* 提示浏览器优化这些属性的变化 */
|
||||
bottom: 0; /* 确保标签底部对齐定位点 */
|
||||
}
|
||||
|
||||
/* 滑入航空器标签样式 */
|
||||
:deep(.vehicle-aircraft-in) {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 滑出航空器标签样式 */
|
||||
:deep(.vehicle-aircraft-out) {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 无人车标签样式 */
|
||||
:deep(.vehicle-unmanned) {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 特勤车标签样式 */
|
||||
:deep(.vehicle-special) {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 摆渡车标签样式 */
|
||||
:deep(.vehicle-shuttle) {
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
322
src/components/map/info/AlarmNotification.vue
Normal file
@ -0,0 +1,322 @@
|
||||
<template>
|
||||
<div class="alarm-notification-container">
|
||||
<!-- 标签页头部 -->
|
||||
<div class="detail-tabs">
|
||||
<div class="tab-header">
|
||||
<div class="tab-list">
|
||||
<div
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === 'all' }"
|
||||
@click="activeTab = 'all'"
|
||||
>
|
||||
全部
|
||||
</div>
|
||||
<div
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === 'car' }"
|
||||
@click="activeTab = 'car'"
|
||||
>
|
||||
车辆冲突
|
||||
</div>
|
||||
<div
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === 'report' }"
|
||||
@click="activeTab = 'report'"
|
||||
>
|
||||
超界告警
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-actions">
|
||||
|
||||
<div class="close-btn" @click="$emit('close')">
|
||||
<i class="close-icon">×</i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 报警列表 -->
|
||||
<div class="alarm-list">
|
||||
<div class="alarm-item" v-for="(item, index) in filteredAlarms" :key="index">
|
||||
<div class="alarm-icon" :class="item.level">
|
||||
<img v-if="item.type === 'car'" src="@/assets/images/clarm_conflict.png" class="alarm-img" alt="车辆冲突" />
|
||||
<img v-else-if="item.type === 'report'" src="@/assets/images/clarm_over.png" class="alarm-img" alt="超界告警" />
|
||||
<i v-else class="alarm-dot"></i>
|
||||
</div>
|
||||
<div class="alarm-content">
|
||||
<div class="alarm-title">
|
||||
{{ item.carId }} ({{ item.carType }}) {{ item.time }} {{ item.description }}
|
||||
</div>
|
||||
<div class="alarm-time">{{ item.date }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="filteredAlarms.length === 0" class="empty-data">
|
||||
暂无告警数据
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
|
||||
// 当前激活的标签页
|
||||
const activeTab = ref('all');
|
||||
|
||||
// 模拟报警数据
|
||||
const alarmList = ref([
|
||||
{
|
||||
carId: 'QN001',
|
||||
carType: '驱鸟车',
|
||||
time: 'T10:20—10: 25与JD5949发生冲',
|
||||
description: '突告警',
|
||||
date: '2025-03-19 10:30',
|
||||
level: 'high',
|
||||
type: 'car'
|
||||
},
|
||||
{
|
||||
carId: 'QN001',
|
||||
carType: '驱鸟车',
|
||||
time: 'T10:20—10: 25与JD5949发生冲',
|
||||
description: '突告警',
|
||||
date: '2025-03-19 10:30',
|
||||
level: 'high',
|
||||
type: 'car'
|
||||
},
|
||||
{
|
||||
carId: 'QN001',
|
||||
carType: '驱鸟车',
|
||||
time: 'T10:20—10: 25闯入电子围',
|
||||
description: '栏监控区域,发生告警',
|
||||
date: '2025-03-19 10:30',
|
||||
level: 'medium',
|
||||
type: 'report'
|
||||
},
|
||||
{
|
||||
carId: 'QN001',
|
||||
carType: '驱鸟车',
|
||||
time: 'T10:20—10: 25闯入电子围',
|
||||
description: '栏监控区域,发生告警',
|
||||
date: '2025-03-19 10:30',
|
||||
level: 'medium',
|
||||
type: 'report'
|
||||
}
|
||||
]);
|
||||
|
||||
// 根据当前选中的标签页过滤告警数据
|
||||
const filteredAlarms = computed(() => {
|
||||
if (activeTab.value === 'all') {
|
||||
return alarmList.value;
|
||||
}
|
||||
return alarmList.value.filter(item => item.type === activeTab.value);
|
||||
});
|
||||
|
||||
// 暴露方法,允许父组件更新数据
|
||||
defineExpose({
|
||||
updateAlarmList(newList) {
|
||||
if (newList && Array.isArray(newList)) {
|
||||
alarmList.value = newList;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.alarm-notification-container {
|
||||
position: absolute;
|
||||
left:70px;
|
||||
top: 15%;
|
||||
width: 360px;
|
||||
background-color: rgba(41, 44, 56, 0.95);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
color: #fff;
|
||||
z-index: 1000;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.detail-tabs {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tab-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
/* background-color: #1E2233; */
|
||||
border-top-left-radius: 8px;
|
||||
border-top-right-radius: 8px;
|
||||
padding: 0 10px;
|
||||
height: 34px;
|
||||
position: relative;
|
||||
border-bottom: 1px solid #3A4452;
|
||||
}
|
||||
|
||||
.tab-list {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.tab-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
font-size: 20px;
|
||||
color: #A0A8B7;
|
||||
margin-right: 10px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.add-btn:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
padding: 0 16px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
color: #A0A8B7;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.tab-item.active {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.tab-item.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background-color: #347AE2;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* .close-btn:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
} */
|
||||
|
||||
.close-icon {
|
||||
font-size: 20px;
|
||||
color: #A0A8B7;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.alarm-list {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.alarm-item {
|
||||
display: flex;
|
||||
padding: 12px 15px;
|
||||
border-bottom: 1px solid #3A4452;
|
||||
}
|
||||
|
||||
.alarm-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
margin-right: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.alarm-img {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.alarm-icon.high {
|
||||
background-color: rgba(255, 77, 79, 0.2);
|
||||
}
|
||||
|
||||
.alarm-icon.medium {
|
||||
background-color: rgba(255, 153, 0, 0.2);
|
||||
}
|
||||
|
||||
.alarm-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.alarm-icon.high .alarm-dot {
|
||||
background-color: #FF4D4F;
|
||||
}
|
||||
|
||||
.alarm-icon.medium .alarm-dot {
|
||||
background-color: #FF9900;
|
||||
}
|
||||
|
||||
.alarm-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.alarm-title {
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.alarm-time {
|
||||
font-size: 12px;
|
||||
color: #8C959F;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.empty-data {
|
||||
padding: 30px 0;
|
||||
text-align: center;
|
||||
color: #8C959F;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
.alarm-list::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
}
|
||||
|
||||
.alarm-list::-webkit-scrollbar-track {
|
||||
background: rgba(19, 26, 36, 0.5);
|
||||
}
|
||||
|
||||
.alarm-list::-webkit-scrollbar-thumb {
|
||||
background: rgba(78, 113, 143, 0.5);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.alarm-list::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(78, 113, 143, 0.8);
|
||||
}
|
||||
</style>
|
||||
452
src/components/map/info/CarDetail.vue
Normal file
@ -0,0 +1,452 @@
|
||||
<template>
|
||||
<div class="car-detail-container">
|
||||
<!-- 顶部返回与标题 -->
|
||||
|
||||
<div class="header">
|
||||
<img
|
||||
src="../../../assets/images/sub_icon.png"
|
||||
alt="list_bg"
|
||||
class="header-title-icon"
|
||||
/>
|
||||
<span class="header-title">车辆详情</span>
|
||||
</div>
|
||||
|
||||
<div class="back-button">
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
class="btn custom-back-btn"
|
||||
size="small"
|
||||
@click="$emit('back')"
|
||||
>
|
||||
<el-icon class="back-icon"><ArrowLeft /></el-icon>
|
||||
返回车辆列表
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="track-list">
|
||||
<!-- 车辆基本信息 -->
|
||||
<div class=" section base-info">
|
||||
<div class="car-id">
|
||||
{{ car.id }}({{ car.type }})
|
||||
<div class="video-tabs">
|
||||
<span class="tab active">任务中</span>
|
||||
<span class="tab active">在线</span>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<div>品牌:{{ car.brand || "驱鸟车" }}</div>
|
||||
<div>负责人:{{ car.manager }}</div>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<div>所属单位:{{ car.company || "机场发展公司" }}</div>
|
||||
<div>联系方式:{{ car.phone || "18661190988" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 任务信息 -->
|
||||
<div class="section task-info">
|
||||
<div class="section-title">任务信息</div>
|
||||
<div class="info-row">
|
||||
<div>开始时间:{{ car.taskStart || "5月1日12:00" }}</div>
|
||||
<div>结束时间:{{ car.taskEnd || "5月1日12:30" }}</div>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<div>起点:{{ car.taskStartPoint || "A区快冲桩" }}</div>
|
||||
<div>终点:{{ car.taskEndPoint || "B区快冲桩" }}</div>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<div>行驶速度:{{ car.speed }}</div>
|
||||
<div>行驶里程:{{ car.distance }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 电池监控 -->
|
||||
<div class="section battery-info">
|
||||
<div class="section-title">
|
||||
电池监控 <span class="status-normal">正常</span>
|
||||
</div>
|
||||
<div class="info-row1">
|
||||
|
||||
<div class="battery-left">
|
||||
<img src="../../../assets/images/battery.png" alt="battery" class="battery-icon">
|
||||
<p class="battery-content">电池温度</p>
|
||||
</div>
|
||||
<p class="battery-temp" :class="getBatteryTempClass(car.batteryTemp || '35.2℃')">{{ car.batteryTemp || "35.2℃" }}</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 360°监控状态 -->
|
||||
<div class="section video-info">
|
||||
<div class="section-title">
|
||||
360°监控状态
|
||||
<span class="video-tabs">
|
||||
<span class="tab active">前视</span>
|
||||
<span class="tab">左前</span>
|
||||
<span class="tab">右前</span>
|
||||
<span class="tab">后视</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="video-preview">
|
||||
<img :src="car.videoImg" alt="360监控" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 历史轨迹 -->
|
||||
<div class="section track-info">
|
||||
<div class="section-title">历史轨迹(10)</div>
|
||||
<div class="track-search">
|
||||
<input type="text" placeholder="请输入关键字搜索" />
|
||||
<span class="search-icon"></span>
|
||||
</div>
|
||||
|
||||
|
||||
<div
|
||||
class="track-item"
|
||||
v-for="(item, idx) in car.trackList || defaultTrackList"
|
||||
:key="idx"
|
||||
>
|
||||
<div class="track-timeline">
|
||||
<div class="timeline-dot" :class="{ 'active': idx === 0 }"></div>
|
||||
<div class="timeline-line" v-if="idx !== (car.trackList || defaultTrackList).length - 1"></div>
|
||||
</div>
|
||||
<div class="track-content">
|
||||
<div class="track-time">{{ item.time }}</div>
|
||||
<div class="track-desc">{{ item.desc }}</div>
|
||||
<div class="track-detail">
|
||||
<span>行驶里程:{{ item.distance }}</span>
|
||||
<span>任务编号:{{ item.taskId }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
car: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const defaultTrackList = [
|
||||
{
|
||||
time: "2024年6月16日 18:12:09—2024年6月16日 20:12:09",
|
||||
desc: "起点:机场发展公司 终点:机场发展公司",
|
||||
distance: "20km",
|
||||
taskId: "001",
|
||||
},
|
||||
{
|
||||
time: "2024年6月15日 18:12:09—2024年6月15日 20:12:09",
|
||||
desc: "起点:机场发展公司 终点:机场发展公司",
|
||||
distance: "20km",
|
||||
taskId: "002",
|
||||
},
|
||||
];
|
||||
|
||||
// 根据温度判断状态类名
|
||||
function getBatteryTempClass(temp) {
|
||||
// 提取温度数值
|
||||
const tempValue = parseFloat(temp);
|
||||
|
||||
if (isNaN(tempValue)) return 'temp-normal';
|
||||
|
||||
if (tempValue >= 45) return 'temp-danger';
|
||||
if (tempValue >= 38) return 'temp-warning';
|
||||
return 'temp-normal';
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.car-detail-container {
|
||||
width: 391px;
|
||||
height: 85vh;
|
||||
overflow-y: hidden;
|
||||
background: #4F565F;
|
||||
border-radius: 8px;
|
||||
color: #fff;
|
||||
/* padding: 18px 20px 20px 20px; */
|
||||
font-size: 14px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top:0;
|
||||
z-index: 10;
|
||||
}
|
||||
.header-actions {
|
||||
padding: 16px;
|
||||
}
|
||||
/* 返回按钮样式 */
|
||||
/* .back-button {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
z-index: 10;
|
||||
} */
|
||||
|
||||
/* 自定义返回按钮样式 */
|
||||
.custom-back-btn {
|
||||
margin: 10px;
|
||||
background-color: #424851 !important;
|
||||
border-color: #303236 !important;
|
||||
color: #ffffff !important;
|
||||
padding: 5px;
|
||||
line-height: 1.5;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.custom-back-btn:hover {
|
||||
background-color: rgba(52, 122, 226, 0.3) !important;
|
||||
}
|
||||
|
||||
.back-icon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
line-height: 1.8;
|
||||
font-weight: bold;
|
||||
background: url("../../../assets/images/subheading.png") no-repeat;
|
||||
background-size: 100% 100%;
|
||||
padding: 5px 10px;
|
||||
color: #fff;
|
||||
img {
|
||||
width: 31px;
|
||||
height: 15px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
.back-btn {
|
||||
color: #fff;
|
||||
background: #424851;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
/* display: inline-block; */
|
||||
padding: 5px 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.header-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.section {
|
||||
width:95%;
|
||||
margin:0 auto;
|
||||
background: #424851;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 16px;
|
||||
padding: 14px 16px 10px 16px;
|
||||
}
|
||||
.base-info .car-id {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 6px;
|
||||
|
||||
}
|
||||
.info-row1 {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 6px;
|
||||
border:1px solid #222b36;
|
||||
background: #3B4047;
|
||||
border-left: 4px solid #303236;
|
||||
padding:5px 10px;
|
||||
|
||||
p{
|
||||
margin:0 !important;
|
||||
}
|
||||
|
||||
}
|
||||
.battery-left{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap:10px;
|
||||
}
|
||||
.battery-content{
|
||||
color: #fff;
|
||||
}
|
||||
.section-title {
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.status-normal {
|
||||
background: #1fcb81;
|
||||
color: #fff;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
padding: 2px 8px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
.battery-temp {
|
||||
font-weight: bold;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.temp-normal {
|
||||
color: #1fcb81;
|
||||
}
|
||||
|
||||
.temp-warning {
|
||||
color: #FDB92C;
|
||||
}
|
||||
|
||||
.temp-danger {
|
||||
color: #FF312F;
|
||||
}
|
||||
.video-info {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.video-tabs {
|
||||
margin-left: 18px;
|
||||
}
|
||||
.tab {
|
||||
display: inline-block;
|
||||
padding: 2px 10px;
|
||||
border-radius: 3px;
|
||||
background: #222b36;
|
||||
color: #8ec6ff;
|
||||
margin-left: 6px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.tab.active {
|
||||
background: #5690E7;
|
||||
color: #fff;
|
||||
}
|
||||
.video-preview {
|
||||
width:100%;
|
||||
height: 200px;
|
||||
aspect-ratio:4/3;
|
||||
object-fit: cover;
|
||||
|
||||
text-align: center;
|
||||
border:1px solid #222b36;
|
||||
}
|
||||
.video-preview img {
|
||||
width: 95%;
|
||||
border-radius: 6px;
|
||||
background: #222b36;
|
||||
|
||||
}
|
||||
.track-info {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.track-search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.track-search input {
|
||||
background: #222b36;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 5px 30px 5px 10px;
|
||||
color: #fff;
|
||||
width: 100%;
|
||||
font-size: 13px;
|
||||
outline: none;
|
||||
}
|
||||
.search-icon {
|
||||
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='%238EC6FF'%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;
|
||||
margin-left: -26px;
|
||||
}
|
||||
.track-list {
|
||||
height: 70vh;
|
||||
/* flex: 1; */
|
||||
overflow-y: auto;
|
||||
|
||||
}
|
||||
.track-item {
|
||||
border-radius: 4px;
|
||||
padding: 7px 10px;
|
||||
margin-bottom: 7px;
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.track-timeline {
|
||||
width: 20px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.timeline-dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background-color: #8ec6ff;
|
||||
margin-top: 4px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.timeline-dot.active {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background-color: #1a6dff;
|
||||
box-shadow: 0 0 8px 2px rgba(26, 109, 255, 0.6);
|
||||
}
|
||||
|
||||
.timeline-line {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 2px;
|
||||
height: calc(100% + 7px);
|
||||
background: linear-gradient(to bottom, #8ec6ff 50%, transparent 50%);
|
||||
background-size: 2px 8px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.track-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.track-time {
|
||||
color: #F0F0F0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.track-desc {
|
||||
margin: 2px 0 2px 0;
|
||||
color:#C3C3C3;
|
||||
}
|
||||
|
||||
.track-detail {
|
||||
color: #b0b8c5;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
145
src/components/map/info/FilterDropdown.vue
Normal file
@ -0,0 +1,145 @@
|
||||
<template>
|
||||
<div class="filter-dropdown">
|
||||
<div class="selected-filter" @click="toggleDropdown">
|
||||
{{ selectedOption }}
|
||||
<span class="arrow-down"></span>
|
||||
</div>
|
||||
<div class="dropdown-menu" v-show="isOpen">
|
||||
<div
|
||||
v-for="(option, index) in options"
|
||||
:key="index"
|
||||
class="menu-item"
|
||||
@click="selectOption(option)"
|
||||
>
|
||||
{{ option.label }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, onMounted, onBeforeUnmount } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
options: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => []
|
||||
},
|
||||
modelValue: {
|
||||
type: [String, Number, Object],
|
||||
default: ''
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '全部'
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'change']);
|
||||
|
||||
const isOpen = ref(false);
|
||||
const selectedOption = ref(props.placeholder);
|
||||
|
||||
// 初始化选中项
|
||||
onMounted(() => {
|
||||
if (props.modelValue) {
|
||||
const option = props.options.find(opt => opt.value === props.modelValue);
|
||||
if (option) {
|
||||
selectedOption.value = option.label;
|
||||
}
|
||||
}
|
||||
|
||||
// 添加点击外部关闭下拉菜单的事件监听
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
// 移除事件监听
|
||||
document.removeEventListener('click', handleClickOutside);
|
||||
});
|
||||
|
||||
// 监听modelValue变化
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
if (newValue) {
|
||||
const option = props.options.find(opt => opt.value === newValue);
|
||||
if (option) {
|
||||
selectedOption.value = option.label;
|
||||
}
|
||||
} else {
|
||||
selectedOption.value = props.placeholder;
|
||||
}
|
||||
});
|
||||
|
||||
// 切换下拉菜单显示状态
|
||||
function toggleDropdown(event) {
|
||||
event.stopPropagation();
|
||||
isOpen.value = !isOpen.value;
|
||||
}
|
||||
|
||||
// 选择选项
|
||||
function selectOption(option) {
|
||||
selectedOption.value = option.label;
|
||||
isOpen.value = false;
|
||||
emit('update:modelValue', option.value);
|
||||
emit('change', option);
|
||||
}
|
||||
|
||||
// 点击外部关闭下拉菜单
|
||||
function handleClickOutside(event) {
|
||||
const dropdown = event.target.closest('.filter-dropdown');
|
||||
if (!dropdown) {
|
||||
isOpen.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.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;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.selected-filter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.arrow-down {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 5px solid transparent;
|
||||
border-right: 5px solid transparent;
|
||||
border-top: 5px solid #fff;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
background: #3B4047;
|
||||
border-radius: 4px;
|
||||
width: 100%;
|
||||
z-index: 10;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
padding: 8px 10px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.menu-item:hover {
|
||||
background: #464C55;
|
||||
}
|
||||
</style>
|
||||
@ -76,7 +76,7 @@ onMounted(() => {
|
||||
.car-alarm-container {
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
top: 50px;
|
||||
top: 5%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap:24px;
|
||||
|
||||
@ -1,90 +1,106 @@
|
||||
<template>
|
||||
<div class="event-list-container">
|
||||
<div class="event-list-header">
|
||||
<div class="header-title">
|
||||
<img src="../../../assets/images/sub_icon.png" alt="list_bg" class="header-title-icon">
|
||||
<div class="header-title-text">车辆列表 ({{ totalCount }})</div>
|
||||
|
||||
</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="event-list-container">
|
||||
<CarDetail v-if="selectedCar" :car="selectedCar" @back="selectedCar = null" />
|
||||
<div v-else class="event-list-header">
|
||||
<div class="header-title">
|
||||
<img src="../../../assets/images/sub_icon.png" alt="list_bg" class="header-title-icon">
|
||||
<div class="header-title-text">车辆列表 ({{ totalCount }})</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 class="header-actions">
|
||||
<FilterDropdown
|
||||
v-model="statusFilter"
|
||||
:options="statusOptions"
|
||||
@change="handleFilterChange"
|
||||
/>
|
||||
<FilterDropdown
|
||||
v-model="typeFilter"
|
||||
:options="typeOptions"
|
||||
@change="handleFilterChange"
|
||||
/>
|
||||
<div class="search-box">
|
||||
<input type="text" placeholder="请输入关键字搜索" />
|
||||
<div class="search-icon"></div>
|
||||
</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 class="event-list-content">
|
||||
<div
|
||||
v-for="(car, index) in filteredCarList"
|
||||
:key="car.id"
|
||||
class="car-item"
|
||||
:class="{ 'fault': car.status === 'fault', 'online': car.status === 'online', 'offline': car.status === 'offline' }"
|
||||
@click="showDetail(car)"
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import CarDetail from './CarDetail.vue';
|
||||
import FilterDropdown from './FilterDropdown.vue';
|
||||
|
||||
// 状态筛选选项
|
||||
const statusOptions = [
|
||||
{ label: '全部', value: 'all' },
|
||||
{ label: '在线', value: 'online' },
|
||||
{ label: '离线', value: 'offline' },
|
||||
{ label: '故障', value: 'fault' }
|
||||
];
|
||||
|
||||
// 车辆类型筛选选项
|
||||
const typeOptions = [
|
||||
{ label: '全部', value: 'all' },
|
||||
{ label: '驱鸟车', value: '驱鸟车' },
|
||||
{ label: '牵引车', value: '牵引车' },
|
||||
{ label: '接驳车', value: '接驳车' }
|
||||
];
|
||||
|
||||
// 当前选中的筛选值
|
||||
const statusFilter = ref('all');
|
||||
const typeFilter = ref('all');
|
||||
|
||||
// 车辆数据
|
||||
const carList = ref([
|
||||
@ -148,6 +164,29 @@ const carList = ref([
|
||||
// 计算总数
|
||||
const totalCount = computed(() => carList.value.length);
|
||||
|
||||
// 筛选后的车辆列表
|
||||
const filteredCarList = computed(() => {
|
||||
return carList.value.filter(car => {
|
||||
// 状态筛选
|
||||
if (statusFilter.value !== 'all' && car.status !== statusFilter.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 类型筛选
|
||||
if (typeFilter.value !== 'all' && car.type !== typeFilter.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
// 处理筛选变化
|
||||
function handleFilterChange() {
|
||||
// 可以在这里添加额外的筛选逻辑
|
||||
console.log('筛选条件变更:', { status: statusFilter.value, type: typeFilter.value });
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
function getStatusText(status) {
|
||||
switch(status) {
|
||||
@ -180,6 +219,11 @@ function updateCarList(newList) {
|
||||
}
|
||||
}
|
||||
|
||||
const selectedCar = ref(null);
|
||||
function showDetail(car) {
|
||||
selectedCar.value = car;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
console.log('车辆列表组件已加载');
|
||||
});
|
||||
@ -187,7 +231,7 @@ onMounted(() => {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.event-list-container {
|
||||
background-color: #27313F;
|
||||
background: #4F565F;
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: 50%;
|
||||
@ -199,7 +243,7 @@ onMounted(() => {
|
||||
/* background-color: rgba(24, 33, 45, 0.85); */
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
z-index: 1000;
|
||||
z-index: 1;
|
||||
color: #fff;
|
||||
/* box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); */
|
||||
}
|
||||
@ -227,59 +271,14 @@ onMounted(() => {
|
||||
align-items: center;
|
||||
padding:0 16px;
|
||||
margin: 10px 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);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
@ -288,7 +287,7 @@ onMounted(() => {
|
||||
border-radius: 4px;
|
||||
padding: 6px 30px 6px 10px;
|
||||
color: #fff;
|
||||
width: 150px;
|
||||
width: 100%;
|
||||
font-size: 13px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@ -5,7 +5,10 @@
|
||||
<el-menu
|
||||
class="sidebar"
|
||||
:default-active="activeMenu"
|
||||
:default-openeds="openedMenus"
|
||||
@select="handleMenuClick"
|
||||
@open="handleSubMenuOpen"
|
||||
@close="handleSubMenuClose"
|
||||
background-color="#292C38"
|
||||
text-color="#96A0B5"
|
||||
active-text-color="#fff"
|
||||
@ -84,6 +87,39 @@ const isCollapse = computed(() => !appStore.sidebar.opened);
|
||||
|
||||
// 当前激活的子菜单
|
||||
const activeSubmenu = ref('');
|
||||
// 当前打开的菜单列表
|
||||
const openedMenus = ref([]);
|
||||
|
||||
// 从localStorage获取已保存的打开菜单
|
||||
const getOpenedMenusFromStorage = () => {
|
||||
const openedMenusStr = localStorage.getItem('openedMenus');
|
||||
return openedMenusStr ? JSON.parse(openedMenusStr) : [];
|
||||
};
|
||||
|
||||
// 初始化打开的菜单列表
|
||||
openedMenus.value = getOpenedMenusFromStorage();
|
||||
|
||||
// 保存打开的菜单到localStorage
|
||||
const saveOpenedMenusToStorage = (menus) => {
|
||||
localStorage.setItem('openedMenus', JSON.stringify(menus));
|
||||
};
|
||||
|
||||
// 处理子菜单打开事件
|
||||
const handleSubMenuOpen = (index) => {
|
||||
if (!openedMenus.value.includes(index)) {
|
||||
openedMenus.value.push(index);
|
||||
saveOpenedMenusToStorage(openedMenus.value);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理子菜单关闭事件
|
||||
const handleSubMenuClose = (index) => {
|
||||
const idx = openedMenus.value.indexOf(index);
|
||||
if (idx !== -1) {
|
||||
openedMenus.value.splice(idx, 1);
|
||||
saveOpenedMenusToStorage(openedMenus.value);
|
||||
}
|
||||
};
|
||||
|
||||
const activeMenu = computed(() => {
|
||||
const { meta, path } = route;
|
||||
@ -111,6 +147,13 @@ function handleMenuClick(key) {
|
||||
function handleSubmenuClick(subKey, parentKey) {
|
||||
const fullPath = getFullPath(parentKey, subKey);
|
||||
activeSubmenu.value = fullPath;
|
||||
|
||||
// 确保父菜单被添加到打开列表中
|
||||
if (parentKey && !openedMenus.value.includes(parentKey)) {
|
||||
openedMenus.value.push(parentKey);
|
||||
saveOpenedMenusToStorage(openedMenus.value);
|
||||
}
|
||||
|
||||
if (isExternal(subKey)) {
|
||||
window.open(subKey, "_blank");
|
||||
} else {
|
||||
@ -165,6 +208,22 @@ function findMenuItemByKey(key) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 查找当前路径所属的父菜单
|
||||
function findParentMenuByPath(path) {
|
||||
if (!path || !sidebarRouters.value) return null;
|
||||
|
||||
for (const item of sidebarRouters.value) {
|
||||
if (item && item.children && item.children.length > 0) {
|
||||
for (const subItem of item.children) {
|
||||
if (subItem && getFullPath(item.path, subItem.path) === path) {
|
||||
return item.path;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 判断父菜单是否应该高亮
|
||||
const isParentActive = (route) => {
|
||||
if (!route || !route.children) return false;
|
||||
@ -175,18 +234,38 @@ const isParentActive = (route) => {
|
||||
});
|
||||
};
|
||||
|
||||
// 初始化时设置当前激活的子菜单
|
||||
// 初始化时设置当前激活的子菜单和展开的菜单
|
||||
onMounted(() => {
|
||||
// 确保从localStorage加载已保存的菜单状态
|
||||
openedMenus.value = getOpenedMenusFromStorage();
|
||||
|
||||
const currentPath = route.path;
|
||||
if (currentPath) {
|
||||
activeSubmenu.value = currentPath;
|
||||
|
||||
// 获取当前路径的父菜单
|
||||
const parentMenu = findParentMenuByPath(currentPath);
|
||||
if (parentMenu) {
|
||||
// 确保当前路径的父菜单被添加到打开列表中
|
||||
if (!openedMenus.value.includes(parentMenu)) {
|
||||
openedMenus.value.push(parentMenu);
|
||||
saveOpenedMenusToStorage(openedMenus.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 监听路由变化,更新激活的子菜单
|
||||
// 监听路由变化,更新激活的子菜单和展开的菜单
|
||||
watch(() => route.path, (newPath) => {
|
||||
if (newPath) {
|
||||
activeSubmenu.value = newPath;
|
||||
|
||||
// 获取当前路径的父菜单
|
||||
const parentMenu = findParentMenuByPath(newPath);
|
||||
if (parentMenu && !openedMenus.value.includes(parentMenu)) {
|
||||
openedMenus.value.push(parentMenu);
|
||||
saveOpenedMenusToStorage(openedMenus.value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -199,6 +278,13 @@ watch(() => permissionStore.sidebarRouters, (newRoutes) => {
|
||||
const currentPath = route.path;
|
||||
if (currentPath) {
|
||||
activeSubmenu.value = currentPath;
|
||||
|
||||
// 获取当前路径的父菜单
|
||||
const parentMenu = findParentMenuByPath(currentPath);
|
||||
if (parentMenu && !openedMenus.value.includes(parentMenu)) {
|
||||
openedMenus.value.push(parentMenu);
|
||||
saveOpenedMenusToStorage(openedMenus.value);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
333
src/utils/test_websocket.html
Normal file
@ -0,0 +1,333 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>冲突检测WebSocket测试工具</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
padding: 20px;
|
||||
}
|
||||
h1 {
|
||||
color: #333;
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.test-section {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
margin: 20px 0;
|
||||
padding: 20px;
|
||||
background: #fafafa;
|
||||
}
|
||||
.test-section h2 {
|
||||
color: #555;
|
||||
margin-top: 0;
|
||||
border-bottom: 2px solid #007bff;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.control-group {
|
||||
margin: 15px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.control-group label {
|
||||
min-width: 120px;
|
||||
font-weight: bold;
|
||||
}
|
||||
select, input, button {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
button {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
button:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
button:disabled {
|
||||
background: #6c757d;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.status {
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
margin: 10px 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
.status.connected {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
.status.disconnected {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
.log-container {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
height: 300px;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
background: white;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
.log-entry {
|
||||
margin: 2px 0;
|
||||
padding: 2px 5px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.log-info { background: #f8f9fa; }
|
||||
.log-success { background: #d4edda; color: #155724; }
|
||||
.log-error { background: #f8d7da; color: #721c24; }
|
||||
.log-warning { background: #fff3cd; color: #856404; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>冲突检测WebSocket测试工具</h1>
|
||||
|
||||
<!-- 冲突检测WebSocket测试 -->
|
||||
<div class="test-section">
|
||||
<h2>🚗 冲突检测WebSocket (原生协议)</h2>
|
||||
|
||||
<div class="control-group">
|
||||
<label>服务器地址:</label>
|
||||
<select id="collisionServerSelect">
|
||||
<option value="ws://10.0.0.124:8080/collision">冲突检测WebSocket</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<button id="collisionConnectBtn" onclick="connectCollisionWebSocket()">连接</button>
|
||||
<button id="collisionDisconnectBtn" onclick="disconnectCollisionWebSocket()" disabled>断开</button>
|
||||
<button onclick="sendCollisionPing()">发送心跳</button>
|
||||
<button onclick="sendCollisionSubscribe()">订阅消息</button>
|
||||
</div>
|
||||
|
||||
<div class="status disconnected" id="collisionStatus">状态:未连接</div>
|
||||
|
||||
<div>
|
||||
<h4>日志:</h4>
|
||||
<div class="log-container" id="collisionLog"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 控制面板 -->
|
||||
<div class="test-section">
|
||||
<h2>🎛️ 控制面板</h2>
|
||||
<div class="control-group">
|
||||
<button onclick="clearLogs()">清空日志</button>
|
||||
<button onclick="showConnectionStatus()">显示连接状态</button>
|
||||
<button onclick="performStressTest()">压力测试</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 全局变量
|
||||
let collisionWebSocket = null;
|
||||
let reconnectAttempts = 0;
|
||||
const maxReconnectAttempts = 5;
|
||||
|
||||
// 通用日志函数
|
||||
function log(containerId, message, type = 'info') {
|
||||
const container = document.getElementById(containerId);
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
const entry = document.createElement('div');
|
||||
entry.className = `log-entry log-${type}`;
|
||||
entry.innerHTML = `[${timestamp}] ${message}`;
|
||||
container.appendChild(entry);
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
|
||||
// 更新连接状态
|
||||
function updateStatus(statusId, connected, message = '') {
|
||||
const statusDiv = document.getElementById(statusId);
|
||||
statusDiv.className = `status ${connected ? 'connected' : 'disconnected'}`;
|
||||
statusDiv.textContent = `状态:${connected ? '已连接' : '未连接'}${message ? ' - ' + message : ''}`;
|
||||
}
|
||||
|
||||
// =================== 冲突检测WebSocket ===================
|
||||
function connectCollisionWebSocket() {
|
||||
if (collisionWebSocket && collisionWebSocket.readyState === WebSocket.OPEN) {
|
||||
log('collisionLog', 'WebSocket已经连接', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const serverUrl = document.getElementById('collisionServerSelect').value;
|
||||
log('collisionLog', `连接到: ${serverUrl}`, 'info');
|
||||
|
||||
try {
|
||||
collisionWebSocket = new WebSocket(serverUrl);
|
||||
|
||||
collisionWebSocket.onopen = function(event) {
|
||||
log('collisionLog', '冲突检测WebSocket连接成功!', 'success');
|
||||
updateStatus('collisionStatus', true, '冲突检测');
|
||||
document.getElementById('collisionConnectBtn').disabled = true;
|
||||
document.getElementById('collisionDisconnectBtn').disabled = false;
|
||||
reconnectAttempts = 0;
|
||||
};
|
||||
|
||||
collisionWebSocket.onmessage = function(event) {
|
||||
log('collisionLog', `收到消息: ${event.data}`, 'success');
|
||||
try {
|
||||
const message = JSON.parse(event.data);
|
||||
handleCollisionMessage(message);
|
||||
} catch (e) {
|
||||
log('collisionLog', `非JSON消息: ${event.data}`, 'info');
|
||||
}
|
||||
};
|
||||
|
||||
collisionWebSocket.onerror = function(error) {
|
||||
log('collisionLog', `连接错误: ${error}`, 'error');
|
||||
updateStatus('collisionStatus', false);
|
||||
};
|
||||
|
||||
collisionWebSocket.onclose = function(event) {
|
||||
log('collisionLog', `连接关闭: ${event.code} - ${event.reason}`, 'warning');
|
||||
updateStatus('collisionStatus', false);
|
||||
document.getElementById('collisionConnectBtn').disabled = false;
|
||||
document.getElementById('collisionDisconnectBtn').disabled = true;
|
||||
|
||||
// 自动重连
|
||||
if (reconnectAttempts < maxReconnectAttempts) {
|
||||
reconnectAttempts++;
|
||||
log('collisionLog', `尝试重连 (${reconnectAttempts}/${maxReconnectAttempts})`, 'info');
|
||||
setTimeout(connectCollisionWebSocket, 3000);
|
||||
}
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
log('collisionLog', `连接失败: ${error.message}`, 'error');
|
||||
updateStatus('collisionStatus', false);
|
||||
}
|
||||
}
|
||||
|
||||
function disconnectCollisionWebSocket() {
|
||||
if (collisionWebSocket) {
|
||||
collisionWebSocket.close();
|
||||
collisionWebSocket = null;
|
||||
log('collisionLog', '主动断开连接', 'info');
|
||||
reconnectAttempts = maxReconnectAttempts;
|
||||
}
|
||||
}
|
||||
|
||||
function sendCollisionPing() {
|
||||
if (collisionWebSocket && collisionWebSocket.readyState === WebSocket.OPEN) {
|
||||
collisionWebSocket.send('ping');
|
||||
log('collisionLog', '发送心跳: ping', 'info');
|
||||
} else {
|
||||
log('collisionLog', 'WebSocket未连接', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function sendCollisionSubscribe() {
|
||||
if (collisionWebSocket && collisionWebSocket.readyState === WebSocket.OPEN) {
|
||||
const message = JSON.stringify({
|
||||
type: 'subscribe',
|
||||
topics: ['position_update', 'collision_warning', 'rule_violation'],
|
||||
timestamp: Date.now()
|
||||
});
|
||||
collisionWebSocket.send(message);
|
||||
log('collisionLog', '发送订阅请求', 'info');
|
||||
} else {
|
||||
log('collisionLog', 'WebSocket未连接', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function handleCollisionMessage(message) {
|
||||
switch (message.type) {
|
||||
case 'connection':
|
||||
log('collisionLog', `连接确认: ${message.message}`, 'success');
|
||||
break;
|
||||
case 'pong':
|
||||
log('collisionLog', '心跳响应', 'success');
|
||||
break;
|
||||
case 'position_update':
|
||||
// 修复:使用正确的字段名 object_id 而不是 vehicleId
|
||||
const objectId = message.payload?.object_id || '未知';
|
||||
const objectType = message.payload?.object_type || '';
|
||||
log('collisionLog', `位置更新: ${objectId} (${objectType})`, 'info');
|
||||
break;
|
||||
case 'collision_warning':
|
||||
log('collisionLog', `碰撞预警: ${message.payload?.message || JSON.stringify(message.payload)}`, 'error');
|
||||
break;
|
||||
case 'rule_violation':
|
||||
log('collisionLog', `规则违规: ${message.payload?.ruleType || '未知规则'}`, 'warning');
|
||||
break;
|
||||
case 'rule_execution_status':
|
||||
log('collisionLog', `规则执行状态: ${message.payload?.status || '未知状态'}`, 'info');
|
||||
break;
|
||||
case 'rule_state_change':
|
||||
log('collisionLog', `规则状态变更: ${message.payload?.newState || '未知状态'}`, 'info');
|
||||
break;
|
||||
case 'vehicle_command':
|
||||
log('collisionLog', `车辆指令: ${message.payload?.commandType || '未知指令'}`, 'info');
|
||||
break;
|
||||
case 'traffic_light_status':
|
||||
log('collisionLog', `红绿灯状态: ${message.payload?.intersection_id || '未知路口'}`, 'info');
|
||||
break;
|
||||
default:
|
||||
log('collisionLog', `未知消息类型: ${message.type}`, 'info');
|
||||
}
|
||||
}
|
||||
|
||||
// =================== 控制面板 ===================
|
||||
function clearLogs() {
|
||||
document.getElementById('collisionLog').innerHTML = '';
|
||||
log('collisionLog', '日志已清空', 'info');
|
||||
}
|
||||
|
||||
function showConnectionStatus() {
|
||||
const collisionStatus = collisionWebSocket ?
|
||||
(collisionWebSocket.readyState === WebSocket.OPEN ? '已连接' : '未连接') : '未初始化';
|
||||
|
||||
log('collisionLog', `冲突检测WebSocket: ${collisionStatus}`, 'info');
|
||||
}
|
||||
|
||||
function performStressTest() {
|
||||
log('collisionLog', '开始压力测试...', 'info');
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
setTimeout(() => {
|
||||
if (collisionWebSocket && collisionWebSocket.readyState === WebSocket.OPEN) {
|
||||
collisionWebSocket.send(`ping_${i}`);
|
||||
}
|
||||
}, i * 100);
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载完成
|
||||
window.onload = function() {
|
||||
log('collisionLog', '冲突检测WebSocket测试就绪', 'info');
|
||||
};
|
||||
|
||||
// 页面关闭时清理连接
|
||||
window.onbeforeunload = function() {
|
||||
if (collisionWebSocket) collisionWebSocket.close();
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
88
src/utils/websocket.js
Normal file
@ -0,0 +1,88 @@
|
||||
class WebSocketService {
|
||||
constructor(url, options = {}) {
|
||||
this.url = url;
|
||||
this.ws = null;
|
||||
this.reconnectInterval = options.reconnectInterval || 3000;
|
||||
this.maxReconnectAttempts = options.maxReconnectAttempts || 10;
|
||||
this.reconnectAttempts = 0;
|
||||
this.eventListeners = {};
|
||||
this.isManuallyClosed = false;
|
||||
this.connect();
|
||||
}
|
||||
|
||||
connect() {
|
||||
this.ws = new window.WebSocket(this.url);
|
||||
this.ws.onopen = (event) => {
|
||||
this.reconnectAttempts = 0;
|
||||
this.emit('open', event);
|
||||
};
|
||||
this.ws.onmessage = (event) => {
|
||||
this.emit('message', event.data);
|
||||
};
|
||||
this.ws.onerror = (event) => {
|
||||
this.emit('error', event);
|
||||
};
|
||||
this.ws.onclose = (event) => {
|
||||
this.emit('close', event);
|
||||
if (!this.isManuallyClosed) {
|
||||
this.reconnect();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
reconnect() {
|
||||
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
||||
setTimeout(() => {
|
||||
this.reconnectAttempts++;
|
||||
this.connect();
|
||||
}, this.reconnectInterval);
|
||||
} else {
|
||||
this.emit('reconnect_failed');
|
||||
}
|
||||
}
|
||||
|
||||
send(data) {
|
||||
if (this.ws && this.ws.readyState === window.WebSocket.OPEN) {
|
||||
this.ws.send(typeof data === 'string' ? data : JSON.stringify(data));
|
||||
} else {
|
||||
this.emit('send_failed', data);
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
this.isManuallyClosed = true;
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
}
|
||||
}
|
||||
|
||||
on(event, callback) {
|
||||
if (!this.eventListeners[event]) {
|
||||
this.eventListeners[event] = [];
|
||||
}
|
||||
this.eventListeners[event].push(callback);
|
||||
}
|
||||
|
||||
off(event, callback) {
|
||||
if (!this.eventListeners[event]) return;
|
||||
this.eventListeners[event] = this.eventListeners[event].filter(cb => cb !== callback);
|
||||
}
|
||||
|
||||
emit(event, ...args) {
|
||||
if (this.eventListeners[event]) {
|
||||
this.eventListeners[event].forEach(cb => cb(...args));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 单例模式,方便全局使用
|
||||
let wsInstance = null;
|
||||
|
||||
export function createWebSocket(url, options) {
|
||||
if (!wsInstance) {
|
||||
wsInstance = new WebSocketService(url, options);
|
||||
}
|
||||
return wsInstance;
|
||||
}
|
||||
|
||||
export default WebSocketService;
|
||||
@ -5,12 +5,18 @@
|
||||
<div class="detail-header">
|
||||
<!-- 返回按钮 - 添加在右上角 -->
|
||||
<div class="back-button">
|
||||
<el-button type="primary" plain class="btn custom-back-btn" size="small" @click="$emit('close')">
|
||||
<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="车辆图片" />
|
||||
@ -20,62 +26,120 @@
|
||||
<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 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>
|
||||
<!-- 下部分:标签页 -->
|
||||
<div class="detail-tabs">
|
||||
<!-- 分屏按钮只在视频监控tab下显示,保持绝对定位 -->
|
||||
<div v-if="activeTab === 'video'" class="split-btns">
|
||||
<el-tooltip content="单画面" placement="top">
|
||||
<img
|
||||
src="../../../assets/images/1btn.png"
|
||||
alt="单画面"
|
||||
@click.stop="layoutType = '1'"
|
||||
/>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="双画面" placement="top">
|
||||
<img
|
||||
src="../../../assets/images/2btn.png"
|
||||
alt="双画面"
|
||||
@click.stop="layoutType = '2'"
|
||||
/>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="六画面" placement="top">
|
||||
<img
|
||||
src="../../../assets/images/6btn.png"
|
||||
alt="六画面"
|
||||
@click.stop="layoutType = '6'"
|
||||
/>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-tabs v-model="activeTab" class="demo-tabs" @tab-click="handleClick">
|
||||
<el-tab-pane label="电池概况" name="battery">
|
||||
<template #label>
|
||||
<span class="custom-tabs-label">
|
||||
<img src="../../../assets/images/tab1.png" alt="电池概况" />
|
||||
<span>电池概况</span>
|
||||
</span>
|
||||
</template>
|
||||
<BatteryOverview :vehicle="vehicle" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="故障报警" name="fault">
|
||||
<template #label>
|
||||
<span class="custom-tabs-label">
|
||||
<img src="../../../assets/images/tab2.png" alt="故障报警" />
|
||||
<span>故障报警</span>
|
||||
</span>
|
||||
</template>
|
||||
<FaultAlarm :vehicle="vehicle" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="轨迹回放" name="track">
|
||||
<template #label>
|
||||
<span class="custom-tabs-label">
|
||||
<img src="../../../assets/images/tab3.png" alt="轨迹回放" />
|
||||
<span>轨迹回放</span>
|
||||
</span>
|
||||
</template>
|
||||
<TrackPlayback :vehicle="vehicle" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="充放电统计" name="charging">
|
||||
<template #label>
|
||||
<span class="custom-tabs-label">
|
||||
<img src="../../../assets/images/tab4.png" alt="充放电统计" />
|
||||
<span>充放电统计</span>
|
||||
</span>
|
||||
</template>
|
||||
<ChargingStats :vehicle="vehicle" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="视频监控" name="video">
|
||||
<VideoMonitoring :vehicle="vehicle" />
|
||||
<template #label>
|
||||
<span class="custom-tabs-label">
|
||||
<img src="../../../assets/images/tab5.png" alt="视频监控" />
|
||||
<span>视频监控</span>
|
||||
</span>
|
||||
</template>
|
||||
<VideoMonitoring :vehicle="vehicle" :layout-type="layoutType" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
@ -84,7 +148,7 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, defineProps, onMounted } from "vue";
|
||||
import { ArrowLeft } from '@element-plus/icons-vue';
|
||||
import { ArrowLeft, Monitor, Grid } 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";
|
||||
@ -105,6 +169,7 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
const activeTab = ref("battery");
|
||||
const layoutType = ref("6");
|
||||
const cardList = [
|
||||
{ name: "驱动车", value: props.vehicle?.type || "车型类型", icon: carIcon1 },
|
||||
{ name: "比亚迪", value: props.vehicle?.brand || "出场品牌", icon: carIcon2 },
|
||||
@ -141,7 +206,7 @@ const cardList = [
|
||||
background-color: #292c38;
|
||||
padding: 24px;
|
||||
justify-content: flex-start;
|
||||
gap:20px;
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
@ -157,7 +222,7 @@ const cardList = [
|
||||
/* 自定义返回按钮样式 */
|
||||
.custom-back-btn {
|
||||
background-color: rgba(52, 122, 226, 0.2) !important;
|
||||
border-color: #347AE2 !important;
|
||||
border-color: #347ae2 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
@ -178,7 +243,6 @@ const cardList = [
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
}
|
||||
.vehicle-image img {
|
||||
width: 100%;
|
||||
@ -186,16 +250,14 @@ const cardList = [
|
||||
object-fit: cover;
|
||||
}
|
||||
.vehicle-info-panel {
|
||||
|
||||
/* width: 60%; */
|
||||
flex: 1;
|
||||
display: flex;
|
||||
/* flex-direction: column; */
|
||||
justify-content: center;
|
||||
}
|
||||
.info-bottom-row{
|
||||
.info-bottom-row {
|
||||
width: 100%;
|
||||
|
||||
}
|
||||
.info-top-row {
|
||||
display: flex;
|
||||
@ -203,7 +265,6 @@ const cardList = [
|
||||
align-items: center;
|
||||
/* margin-bottom: 8px; */
|
||||
margin-top: 10px;
|
||||
|
||||
}
|
||||
.car-icon {
|
||||
width: 48px;
|
||||
@ -265,7 +326,6 @@ const cardList = [
|
||||
.info-cards-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
|
||||
}
|
||||
.info-card {
|
||||
/* width: 100%; */
|
||||
@ -304,11 +364,31 @@ const cardList = [
|
||||
color: #fff;
|
||||
}
|
||||
.detail-tabs {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
padding:12px 24px;
|
||||
background-color: #292C38;
|
||||
padding: 12px 24px;
|
||||
background-color: #292c38;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.split-btns {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
right: 32px;
|
||||
transform: translateY(80%);
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.custom-tabs-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
img {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
filter: brightness(0) invert(1); /* 默认将图标变为白色 */
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-tabs) {
|
||||
--el-tabs-header-background-color: #1e2233;
|
||||
@ -316,18 +396,22 @@ const cardList = [
|
||||
}
|
||||
:deep(.el-tabs__header) {
|
||||
margin-bottom: 20px;
|
||||
border-bottom: none;
|
||||
border-bottom: none;
|
||||
}
|
||||
:deep(.el-tabs__item) {
|
||||
color: #F0F0F0;
|
||||
color: #f0f0f0;
|
||||
}
|
||||
:deep(.el-tabs__item.is-active) {
|
||||
color: #409eff;
|
||||
|
||||
.custom-tabs-label img {
|
||||
filter: brightness(0) saturate(100%) invert(46%) sepia(85%) saturate(1731%) hue-rotate(199deg) brightness(100%) contrast(101%); /* 选中时图标变为蓝色 #409eff */
|
||||
}
|
||||
}
|
||||
:deep .el-tabs__nav-wrap::after {
|
||||
height: 1px;
|
||||
background-color: #4C4F5F;
|
||||
}
|
||||
height: 1px;
|
||||
background-color: #4c4f5f;
|
||||
}
|
||||
@media (max-width: 992px) {
|
||||
.detail-header {
|
||||
flex-direction: column;
|
||||
|
||||
@ -37,6 +37,7 @@
|
||||
<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>
|
||||
@ -53,7 +54,7 @@
|
||||
@click="handleViewSwitch('list')"
|
||||
title="列表视图"
|
||||
>
|
||||
<svg-icon icon-class="table" />
|
||||
<img src="../../../assets/images/monitor_tab1.png" alt="列表视图" />
|
||||
</div>
|
||||
<div
|
||||
class="view-btn card-view"
|
||||
@ -61,7 +62,7 @@
|
||||
@click="handleViewSwitch('card')"
|
||||
title="卡片视图"
|
||||
>
|
||||
<svg-icon icon-class="dashboard" />
|
||||
<img src="../../../assets/images/monitor_tab2.png" alt="卡片视图" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -137,6 +138,9 @@ const activeStatIndex = ref(0);
|
||||
// 视图模式 - 列表或卡片
|
||||
const viewMode = ref('list');
|
||||
|
||||
// 状态过滤器
|
||||
const statusFilter = ref('default');
|
||||
|
||||
// 表格选中行
|
||||
const selectedRows = ref([]);
|
||||
|
||||
@ -217,6 +221,16 @@ function resetFilter() {
|
||||
function exportVehicleData() {
|
||||
exportData();
|
||||
}
|
||||
|
||||
// 设置状态过滤器
|
||||
function setStatusFilter(status) {
|
||||
statusFilter.value = status;
|
||||
// 根据状态过滤数据
|
||||
updateFilters({
|
||||
...filters.value,
|
||||
status: status === 'default' ? '' : status
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@ -275,6 +289,76 @@ function exportVehicleData() {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
/* 状态选择器样式 */
|
||||
.status-selector {
|
||||
background-color: #343744;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
margin-right: 20px;
|
||||
|
||||
.status-label {
|
||||
color: #96A0B5;
|
||||
font-size: 14px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.status-options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
|
||||
.status-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: #2B3B5A;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: #2B3B5A;
|
||||
|
||||
.status-icon {
|
||||
background-color: #409eff;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
background-color: #292C38;
|
||||
|
||||
img {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: #409eff;
|
||||
}
|
||||
}
|
||||
|
||||
.status-text {
|
||||
color: #96A0B5;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
@ -300,21 +384,18 @@ function exportVehicleData() {
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
|
||||
img {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #2B3B5A;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: #409eff;
|
||||
|
||||
:deep(svg) {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(svg) {
|
||||
font-size: 18px;
|
||||
color: #96A0B5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,25 +6,22 @@
|
||||
<!-- 搜索区域 -->
|
||||
<div class="search-area">
|
||||
<el-input
|
||||
v-model="queryParams.carNo"
|
||||
placeholder="请输入车牌号、品牌、所属单位查询"
|
||||
v-model="queryParams.licensePlateNumber"
|
||||
placeholder="请输入车牌号查询"
|
||||
clearable
|
||||
prefix-icon="Search"
|
||||
class="search-input"
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
|
||||
<el-select v-model="queryParams.type" placeholder="车辆类型" clearable class="search-select">
|
||||
<el-select v-model="queryParams.typeId" placeholder="车辆类型" clearable class="search-select">
|
||||
<el-option label="全部" value="" />
|
||||
<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-option
|
||||
v-for="item in vehicleTypeOptions"
|
||||
:key="item.typeId"
|
||||
:label="item.typeName"
|
||||
:value="item.typeId"
|
||||
/>
|
||||
</el-select>
|
||||
|
||||
<div class="search-buttons">
|
||||
@ -62,18 +59,28 @@
|
||||
<el-button link text type="danger" @click="handleDelete(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="车牌号" prop="carNo" align="left" />
|
||||
<el-table-column label="类型" prop="type" align="left" />
|
||||
<el-table-column label="车牌号" prop="licensePlateNumber" align="left" />
|
||||
<el-table-column label="车辆类型" align="left">
|
||||
<template #default="scope">
|
||||
{{ getVehicleTypeName(scope.row.typeId) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="品牌" prop="brand" align="left" />
|
||||
<el-table-column label="所属单位" prop="organization" align="left" />
|
||||
|
||||
|
||||
|
||||
<el-table-column label="负责人" prop="contact" align="left" />
|
||||
<el-table-column label="负责人电话" prop="phonenumber" align="left" />
|
||||
<!-- 车辆图片 -->
|
||||
<el-table-column label="车辆图片" align="left">
|
||||
<template #default="scope">
|
||||
<img :src="scope.row.imageUrl" class="vehicle-image" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="所属单位" prop="owningUnit" align="left" />
|
||||
<el-table-column label="负责人" prop="contactPerson" align="left" />
|
||||
<el-table-column label="负责人电话" prop="phoneNumber" align="left" />
|
||||
<el-table-column label="创建人" prop="createBy" align="left" />
|
||||
|
||||
<el-table-column label="最新时间" prop="lastUpdateTime" align="left" width="160" />
|
||||
<el-table-column label="创建时间" prop="createTime" align="left" width="160">
|
||||
<template #default="scope">
|
||||
<span>{{ parseTime(scope.row.createTime) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页组件 -->
|
||||
@ -97,13 +104,20 @@
|
||||
<el-form ref="vehicleFormRef" :model="form" :rules="rules" 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 label="车牌号" prop="licensePlateNumber">
|
||||
<el-input v-model="form.licensePlateNumber" placeholder="请输入车牌号" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="车辆类型" prop="type">
|
||||
<el-input v-model="form.type" placeholder="请输入车辆类型" />
|
||||
<el-form-item label="车辆类型" prop="typeId">
|
||||
<el-select v-model="form.typeId" placeholder="请选择车辆类型" style="width: 100%">
|
||||
<el-option
|
||||
v-for="item in vehicleTypeOptions"
|
||||
:key="item.typeId"
|
||||
:label="item.typeName"
|
||||
:value="item.typeId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
@ -113,26 +127,31 @@
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="所属单位" prop="organization">
|
||||
<el-input v-model="form.organization" placeholder="请输入所属单位" />
|
||||
<el-form-item label="所属单位" prop="owningUnit">
|
||||
<el-input v-model="form.owningUnit" placeholder="请输入所属单位" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="24">
|
||||
<el-form-item label="联系人" prop="contact">
|
||||
<el-input v-model="form.contact" placeholder="请输入联系人" />
|
||||
<el-form-item label="联系人" prop="contactPerson">
|
||||
<el-input v-model="form.contactPerson" placeholder="请输入联系人" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="电话" prop="phonenumber">
|
||||
<el-input v-model="form.phonenumber" placeholder="请输入电话" />
|
||||
<el-form-item label="电话" prop="phoneNumber">
|
||||
<el-input v-model="form.phoneNumber" placeholder="请输入电话" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="车辆图片" prop="image">
|
||||
<!-- 创建人 -->
|
||||
<el-form-item label="创建人" prop="createBy">
|
||||
<el-input v-model="form.createBy" placeholder="请输入创建人" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="车辆图片" prop="imageUrl">
|
||||
<div class="avatar-uploader-box">
|
||||
<div v-if="form.image" class="avatar-preview">
|
||||
<img :src="form.image" class="avatar" />
|
||||
<div v-if="form.imageUrl" class="avatar-preview">
|
||||
<img :src="form.imageUrl" class="avatar" />
|
||||
<div class="avatar-replace" @click.stop="handleRemoveImage">
|
||||
<el-icon><Close /></el-icon>
|
||||
</div>
|
||||
@ -150,7 +169,7 @@
|
||||
<el-icon class="avatar-uploader-icon"><Plus /></el-icon>
|
||||
</div>
|
||||
</el-upload>
|
||||
<div class="avatar-count">{{ form.image ? '1/1' : '0/1' }}</div>
|
||||
<div class="avatar-count">{{ form.imageUrl ? '1/1' : '0/1' }}</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
@ -213,12 +232,20 @@
|
||||
</template>
|
||||
|
||||
<script setup name="VehiclePark">
|
||||
import { ref, reactive, onMounted, getCurrentInstance } from 'vue';
|
||||
import { ref, reactive, onMounted, getCurrentInstance, computed } from 'vue';
|
||||
import { getToken } from "@/utils/auth";
|
||||
import { parseTime } from "@/utils/ruoyi";
|
||||
import { Search, Download, Plus, UploadFilled, Close } from '@element-plus/icons-vue';
|
||||
import VehicleStats from '@/components/car/VehicleStats.vue';
|
||||
import Pagination from '@/components/Pagination/index.vue';
|
||||
import {
|
||||
listVehicle_info,
|
||||
getVehicle_info,
|
||||
addVehicle_info,
|
||||
updateVehicle_info,
|
||||
delVehicle_info
|
||||
} from "@/api/system/vehicle_info.js";
|
||||
import { listVehicle_type } from "@/api/system/vehicle_type.js";
|
||||
|
||||
const { proxy } = getCurrentInstance();
|
||||
|
||||
@ -229,6 +256,10 @@ const single = ref(true);
|
||||
const multiple = ref(true);
|
||||
const total = ref(0);
|
||||
|
||||
// 车辆类型选项
|
||||
const vehicleTypeOptions = ref([]);
|
||||
const vehicleTypeMap = ref({});
|
||||
|
||||
// 车辆状态统计数据
|
||||
const vehicleStats = ref([
|
||||
{ title: '在线', count: 45, icon: 'el-icon-success', color: '#67c23a', trend: '+30%', trendUp: true },
|
||||
@ -251,9 +282,8 @@ const statistics = computed(() => {
|
||||
const queryParams = ref({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
carNo: undefined,
|
||||
type: undefined,
|
||||
status: undefined
|
||||
licensePlateNumber: undefined,
|
||||
typeId: undefined
|
||||
});
|
||||
|
||||
// 弹窗控制
|
||||
@ -268,34 +298,34 @@ const importDialog = reactive({
|
||||
|
||||
// 表单数据
|
||||
const form = ref({
|
||||
id: undefined,
|
||||
carNo: '',
|
||||
type: '',
|
||||
vehicleId: undefined,
|
||||
licensePlateNumber: '',
|
||||
typeId: '',
|
||||
brand: '',
|
||||
organization: '',
|
||||
contact: '',
|
||||
phonenumber: '',
|
||||
image: ''
|
||||
owningUnit: '',
|
||||
contactPerson: '',
|
||||
phoneNumber: '',
|
||||
imageUrl: ''
|
||||
});
|
||||
|
||||
// 表单验证规则
|
||||
const rules = {
|
||||
carNo: [
|
||||
licensePlateNumber: [
|
||||
{ required: true, message: "车牌号不能为空", trigger: "blur" }
|
||||
],
|
||||
type: [
|
||||
typeId: [
|
||||
{ required: true, message: "车辆类型不能为空", trigger: "change" }
|
||||
],
|
||||
brand: [
|
||||
{ required: true, message: "车辆品牌不能为空", trigger: "blur" }
|
||||
],
|
||||
organization: [
|
||||
owningUnit: [
|
||||
{ required: true, message: "所属单位不能为空", trigger: "blur" }
|
||||
],
|
||||
contact: [
|
||||
contactPerson: [
|
||||
{ required: true, message: "联系人不能为空", trigger: "blur" }
|
||||
],
|
||||
phonenumber: [
|
||||
phoneNumber: [
|
||||
{ required: true, message: "电话不能为空", trigger: "blur" }
|
||||
]
|
||||
};
|
||||
@ -309,94 +339,10 @@ const upload = reactive({
|
||||
title: "",
|
||||
open: false,
|
||||
// 新增导入文件的接口URL
|
||||
url: import.meta.env.VITE_APP_BASE_API + "/car/park/importData",
|
||||
url: import.meta.env.VITE_APP_BASE_API + "/system/vehicle_info/importData",
|
||||
headers: { Authorization: "Bearer " + getToken() }
|
||||
});
|
||||
|
||||
// 模拟车辆数据
|
||||
const mockVehicleData = [
|
||||
{
|
||||
id: 1,
|
||||
carNo: '鲁B12345',
|
||||
type: '驱动车',
|
||||
brand: '大众',
|
||||
organization: '青岛机场物流公司',
|
||||
faultCount: 0,
|
||||
routeStatus: '任务中',
|
||||
status: '在线',
|
||||
chargeStatus: '未充电',
|
||||
batteryLevel: '89%',
|
||||
location: '跑道区域A',
|
||||
lastUpdateTime: '2024-01-15 10:30:00',
|
||||
contact: '张三',
|
||||
phonenumber: '13800138001'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
carNo: '鲁B67890',
|
||||
type: '巡检车',
|
||||
brand: '金龙',
|
||||
organization: '青岛机场运输公司',
|
||||
faultCount: 1,
|
||||
routeStatus: '待命中',
|
||||
status: '在线',
|
||||
chargeStatus: '未充电',
|
||||
batteryLevel: '65%',
|
||||
location: '航站楼前',
|
||||
lastUpdateTime: '2024-01-16 14:20:00',
|
||||
contact: '李四',
|
||||
phonenumber: '13800138002'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
carNo: '鲁B11111',
|
||||
type: '驱动车',
|
||||
brand: '解放',
|
||||
organization: '青岛机场货运部',
|
||||
faultCount: 3,
|
||||
routeStatus: '充电中',
|
||||
status: '离线',
|
||||
chargeStatus: '充电中',
|
||||
batteryLevel: '25%',
|
||||
location: '充电站',
|
||||
lastUpdateTime: '2024-01-17 09:15:00',
|
||||
contact: '王五',
|
||||
phonenumber: '13800138003'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
carNo: '鲁B22222',
|
||||
type: '巡检车',
|
||||
brand: '宇通',
|
||||
organization: '青岛机场安保部',
|
||||
faultCount: 0,
|
||||
routeStatus: '等待中',
|
||||
status: '故障',
|
||||
chargeStatus: '未充电',
|
||||
batteryLevel: '12%',
|
||||
location: '货运区B',
|
||||
lastUpdateTime: '2024-01-17 16:40:00',
|
||||
contact: '赵六',
|
||||
phonenumber: '13800138004'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
carNo: '鲁B33333',
|
||||
type: '驱动车',
|
||||
brand: '福田',
|
||||
organization: '青岛机场维修部',
|
||||
faultCount: 2,
|
||||
routeStatus: '待命中',
|
||||
status: '在线',
|
||||
chargeStatus: '未充电',
|
||||
batteryLevel: '78%',
|
||||
location: '维修区C',
|
||||
lastUpdateTime: '2024-01-18 08:10:00',
|
||||
contact: '钱七',
|
||||
phonenumber: '13800138005'
|
||||
}
|
||||
];
|
||||
|
||||
/** 获取作业状态对应的类型 */
|
||||
function getRouteStatusType(status) {
|
||||
switch (status) {
|
||||
@ -430,30 +376,16 @@ function getStatusType(status) {
|
||||
/** 查询车辆列表 */
|
||||
function getList() {
|
||||
loading.value = true;
|
||||
setTimeout(() => {
|
||||
let filteredData = [...mockVehicleData];
|
||||
|
||||
if (queryParams.value.carNo) {
|
||||
const keyword = queryParams.value.carNo.toLowerCase();
|
||||
filteredData = filteredData.filter(item =>
|
||||
item.carNo.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);
|
||||
}
|
||||
|
||||
listVehicle_info(queryParams.value).then(res => {
|
||||
loading.value = false;
|
||||
vehicleList.value = filteredData;
|
||||
total.value = filteredData.length;
|
||||
}, 300);
|
||||
vehicleList.value = res.rows || [];
|
||||
|
||||
total.value = res.total || 0;
|
||||
}).catch(() => {
|
||||
loading.value = false;
|
||||
vehicleList.value = [];
|
||||
total.value = 0;
|
||||
});
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
@ -467,16 +399,15 @@ function resetQuery() {
|
||||
queryParams.value = {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
carNo: undefined,
|
||||
type: undefined,
|
||||
status: undefined
|
||||
licensePlateNumber: undefined,
|
||||
typeId: undefined
|
||||
};
|
||||
handleQuery();
|
||||
}
|
||||
|
||||
/** 选择条数 */
|
||||
function handleSelectionChange(selection) {
|
||||
ids.value = selection.map(item => item.id);
|
||||
ids.value = selection.map(item => item.vehicleId);
|
||||
single.value = selection.length != 1;
|
||||
multiple.value = !selection.length;
|
||||
}
|
||||
@ -491,41 +422,46 @@ function handleAdd() {
|
||||
/** 修改按钮操作 */
|
||||
function handleEdit(row) {
|
||||
reset();
|
||||
const id = row.id || ids.value[0];
|
||||
// 模拟获取详情
|
||||
const vehicle = mockVehicleData.find(item => item.id === id);
|
||||
if (vehicle) {
|
||||
Object.assign(form.value, vehicle);
|
||||
}
|
||||
dialog.visible = true;
|
||||
dialog.title = "修改车辆";
|
||||
const vehicleId = row.vehicleId || ids.value[0];
|
||||
getVehicle_info(vehicleId).then(response => {
|
||||
form.value = response.data || {};
|
||||
dialog.visible = true;
|
||||
dialog.title = "修改车辆";
|
||||
});
|
||||
}
|
||||
|
||||
/** 提交按钮 */
|
||||
function submitForm() {
|
||||
proxy.$refs["vehicleFormRef"].validate(valid => {
|
||||
if (valid) {
|
||||
if (form.value.id != null) {
|
||||
if (form.value.vehicleId != undefined) {
|
||||
// 更新
|
||||
proxy.$modal.msgSuccess("修改成功");
|
||||
updateVehicle_info(form.value).then(response => {
|
||||
proxy.$modal.msgSuccess("修改成功");
|
||||
dialog.visible = false;
|
||||
getList();
|
||||
});
|
||||
} else {
|
||||
// 新增
|
||||
proxy.$modal.msgSuccess("新增成功");
|
||||
addVehicle_info(form.value).then(response => {
|
||||
proxy.$modal.msgSuccess("新增成功");
|
||||
dialog.visible = false;
|
||||
getList();
|
||||
});
|
||||
}
|
||||
dialog.visible = false;
|
||||
getList();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** 删除按钮操作 */
|
||||
function handleDelete(row) {
|
||||
const vehicleIds = row.id || ids.value;
|
||||
const vehicleIds = row.vehicleId || ids.value;
|
||||
proxy.$modal.confirm('是否确认删除车辆编号为"' + vehicleIds + '"的数据项?').then(function() {
|
||||
return proxy.$modal.msgSuccess("删除成功");
|
||||
return delVehicle_info(vehicleIds);
|
||||
}).then(() => {
|
||||
getList();
|
||||
});
|
||||
proxy.$modal.msgSuccess("删除成功");
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
/** 导入按钮操作 */
|
||||
@ -555,7 +491,7 @@ function submitFileForm() {
|
||||
|
||||
/** 导出按钮操作 */
|
||||
function handleExport() {
|
||||
proxy.download("car/park/export", {
|
||||
proxy.download("system/vehicle_info/export", {
|
||||
...queryParams.value,
|
||||
pageNum: undefined,
|
||||
pageSize: undefined
|
||||
@ -564,7 +500,7 @@ function handleExport() {
|
||||
|
||||
/** 图片上传成功处理 */
|
||||
function handleImageSuccess(response, file) {
|
||||
form.value.image = response.url;
|
||||
form.value.imageUrl = response.url;
|
||||
}
|
||||
|
||||
/** 图片上传前处理 */
|
||||
@ -584,34 +520,53 @@ function beforeImageUpload(file) {
|
||||
/** 表单重置 */
|
||||
function reset() {
|
||||
form.value = {
|
||||
id: undefined,
|
||||
carNo: '',
|
||||
type: '',
|
||||
vehicleId: undefined,
|
||||
licensePlateNumber: '',
|
||||
typeId: '',
|
||||
brand: '',
|
||||
organization: '',
|
||||
contact: '',
|
||||
phonenumber: '',
|
||||
image: ''
|
||||
owningUnit: '',
|
||||
contactPerson: '',
|
||||
phoneNumber: '',
|
||||
imageUrl: ''
|
||||
};
|
||||
proxy.resetForm("vehicleFormRef");
|
||||
}
|
||||
|
||||
/** 下载模板操作 */
|
||||
function importTemplate() {
|
||||
proxy.download('car/park/importTemplate', {}, `车辆数据模板_${new Date().getTime()}.xlsx`);
|
||||
proxy.download('system/vehicle_info/importTemplate', {}, `车辆数据模板_${new Date().getTime()}.xlsx`);
|
||||
}
|
||||
|
||||
/** 移除图片操作 */
|
||||
function handleRemoveImage() {
|
||||
form.value.image = '';
|
||||
form.value.imageUrl = '';
|
||||
}
|
||||
|
||||
/** 获取车辆类型名称 */
|
||||
function getVehicleTypeName(typeId) {
|
||||
return vehicleTypeMap.value[typeId] || '';
|
||||
}
|
||||
|
||||
/** 获取车辆类型列表 */
|
||||
function getVehicleTypeList() {
|
||||
listVehicle_type().then(res => {
|
||||
if (res.data && Array.isArray(res.data)) {
|
||||
// 过滤出二级类型数据
|
||||
const secondLevelTypes = res.data.filter(item => item.level === 2);
|
||||
vehicleTypeOptions.value = secondLevelTypes;
|
||||
|
||||
// 构建类型ID到类型名称的映射
|
||||
vehicleTypeMap.value = {};
|
||||
secondLevelTypes.forEach(item => {
|
||||
vehicleTypeMap.value[item.typeId] = item.typeName;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getList();
|
||||
// 测试图片显示
|
||||
// form.value.image = 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png';
|
||||
// dialog.visible = true;
|
||||
// dialog.title = "添加车辆";
|
||||
getVehicleTypeList();
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -657,7 +612,11 @@ onMounted(() => {
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.vehicle-image{
|
||||
width:48px;
|
||||
height:48px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
// 输入框 下拉框 搜索栏
|
||||
:deep(.el-input__wrapper:hover) {
|
||||
box-shadow: none !important;
|
||||
|
||||
@ -1,39 +1,87 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<div class="type-container">
|
||||
<!-- Tab栏+右侧按钮 -->
|
||||
<el-tabs
|
||||
v-model="editableTabsValue"
|
||||
|
||||
class="demo-tabs"
|
||||
editable
|
||||
@edit="handleTabsEdit"
|
||||
>
|
||||
<template #add-icon>
|
||||
<el-icon><Select /></el-icon>
|
||||
</template>
|
||||
<el-tab-pane
|
||||
v-for="item in editableTabs"
|
||||
:key="item.name"
|
||||
:label="item.label"
|
||||
:name="item.name"
|
||||
<!-- Tab栏 -->
|
||||
<div class="tabs-container">
|
||||
<el-tabs
|
||||
v-model="editableTabsValue"
|
||||
class="demo-tabs"
|
||||
@tab-click="handleTabClick"
|
||||
>
|
||||
{{ item.content }}
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<!-- 搜索栏和操作按钮 -->
|
||||
<el-tab-pane
|
||||
v-for="item in editableTabs"
|
||||
:key="item.name"
|
||||
:name="item.name"
|
||||
>
|
||||
<template #label>
|
||||
<span class="tab-label-text">{{ item.label }}</span>
|
||||
<span
|
||||
|
||||
class="tab-delete-icon"
|
||||
@click.stop="deleteCustomTab(item.name)"
|
||||
>
|
||||
<img
|
||||
src="../../../assets/images/del.png"
|
||||
alt="删除"
|
||||
class="delete-icon-img"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
{{ item.content }}
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<!-- 添加按钮 -->
|
||||
<img
|
||||
src="../../../assets/images/add.png"
|
||||
class="add-tab-btn"
|
||||
@click="showAddTabDialog = true"
|
||||
alt="add"
|
||||
/>
|
||||
</div>
|
||||
<!-- 新增Tab弹窗 -->
|
||||
<el-dialog
|
||||
v-model="showAddTabDialog"
|
||||
title="新增类型"
|
||||
width="320px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-input
|
||||
v-model="newTabName"
|
||||
placeholder="请输入类型名称"
|
||||
maxlength="10"
|
||||
/>
|
||||
<template #footer>
|
||||
<el-button class="export-btn" @click="showAddTabDialog = false"
|
||||
>取消</el-button
|
||||
>
|
||||
<el-button
|
||||
class="search-btn"
|
||||
type="primary"
|
||||
:disabled="!newTabName.trim() || isTabNameExist"
|
||||
@click="addTab"
|
||||
>确定</el-button
|
||||
>
|
||||
</template>
|
||||
<div
|
||||
v-if="isTabNameExist"
|
||||
style="color: #e74c3c; font-size: 13px; margin-top: 8px"
|
||||
>
|
||||
类型名称已存在
|
||||
</div>
|
||||
</el-dialog>
|
||||
<!-- 其余内容保持不变 -->
|
||||
<div class="search-action-bar">
|
||||
<div class="search-area">
|
||||
<el-input
|
||||
v-model="query"
|
||||
v-model="queryParams.typeName"
|
||||
placeholder="请输入关键字查询"
|
||||
class="search-input"
|
||||
clearable
|
||||
/>
|
||||
<el-button type="primary" class="search-btn" @click="handleSearch"
|
||||
<el-button type="primary" class="search-btn" @click="handleQuery"
|
||||
>搜索</el-button
|
||||
>
|
||||
<el-button class="reset-btn" @click="handleReset">重置</el-button>
|
||||
<el-button class="reset-btn" @click="resetQuery">重置</el-button>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<el-button type="primary" class="add-btn" @click="handleAdd"
|
||||
@ -41,15 +89,16 @@
|
||||
>
|
||||
<el-button
|
||||
class="delete-btn"
|
||||
@click="handleDelete"
|
||||
:disabled="!selected.length"
|
||||
@click="handleBatchDelete"
|
||||
:disabled="multiple"
|
||||
>删除</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 表格 -->
|
||||
<el-table
|
||||
:data="tableData"
|
||||
v-loading="loading"
|
||||
:data="vehicleTypeList"
|
||||
@selection-change="handleSelectionChange"
|
||||
class="custom-table"
|
||||
:header-cell-style="{ backgroundColor: '#343744', color: '#fff' }"
|
||||
@ -57,144 +106,432 @@
|
||||
<el-table-column type="selection" width="50" />
|
||||
<el-table-column label="操作" width="120">
|
||||
<template #default="scope">
|
||||
<el-button link text @click="handleEdit(scope.row)">编辑</el-button>
|
||||
<el-button
|
||||
link
|
||||
text
|
||||
type="danger"
|
||||
@click="handleDelete([scope.row])"
|
||||
<el-button link text @click="handleUpdate(scope.row)"
|
||||
>编辑</el-button
|
||||
>
|
||||
<el-button link text type="danger" @click="handleDelete(scope.row)"
|
||||
>删除</el-button
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="二级类型" prop="type" />
|
||||
<el-table-column label="创建人" prop="creator" />
|
||||
|
||||
<el-table-column label="二级类型" prop="typeName" />
|
||||
<el-table-column label="创建人" prop="createBy" />
|
||||
<el-table-column label="创建时间" prop="createTime" />
|
||||
</el-table>
|
||||
<!-- 分页 -->
|
||||
<pagination
|
||||
v-show="total > 0"
|
||||
:total="total"
|
||||
v-model:page="pageNum"
|
||||
v-model:limit="pageSize"
|
||||
@pagination="handleTabsEdit"
|
||||
v-model:page="queryParams.pageNum"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</div>
|
||||
<!-- 新增/编辑弹窗 -->
|
||||
<el-dialog
|
||||
v-model="open"
|
||||
:title="title"
|
||||
width="500px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form
|
||||
ref="vehicleTypeRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-width="90px"
|
||||
>
|
||||
<el-form-item label="二级类型" prop="typeName">
|
||||
<el-input v-model="form.typeName" placeholder="请输入" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="cancel">取消</el-button>
|
||||
<el-button type="primary" @click="submitForm">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from "vue";
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted } from "vue";
|
||||
import { Select } from "@element-plus/icons-vue";
|
||||
let tabIndex = 2;
|
||||
const editableTabsValue = ref("2");
|
||||
import Pagination from "../../../components/Pagination/index.vue";
|
||||
import { getCurrentInstance } from "vue";
|
||||
import {
|
||||
listVehicle_type,
|
||||
getVehicle_type,
|
||||
addVehicle_type,
|
||||
updateVehicle_type,
|
||||
delVehicle_type,
|
||||
} from "@/api/system/vehicle_type.js";
|
||||
|
||||
const { proxy } = getCurrentInstance();
|
||||
|
||||
// Tab相关
|
||||
const editableTabsValue = ref("");
|
||||
const editableTabs = ref([
|
||||
{ label: "无人车", name: "无人车",content:'' },
|
||||
{ label: "特勤车", name: "特勤车",content:''},
|
||||
{ label: "普通车", name: "普通车" ,content:''},
|
||||
{ label: "无人车", name: "无人车", content: "" },
|
||||
{ label: "特勤车", name: "特勤车", content: "" },
|
||||
{ label: "普通车", name: "普通车", content: "" },
|
||||
]);
|
||||
|
||||
const activeTab = ref("无人车");
|
||||
const query = ref("");
|
||||
const pageNum = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const total = ref(0);
|
||||
const selected = ref([]);
|
||||
const showAddTabDialog = ref(false);
|
||||
const newTabName = ref("");
|
||||
|
||||
const mockData = ref([
|
||||
{
|
||||
tab: "无人车",
|
||||
list: [
|
||||
{
|
||||
id: 1,
|
||||
type: "驱动车",
|
||||
creator: "系统管理员",
|
||||
createTime: "2024-08-17 15:48:30",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: "牵引车",
|
||||
creator: "系统管理员",
|
||||
createTime: "2024-08-17 15:48:30",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
tab: "特勤车",
|
||||
list: [
|
||||
{
|
||||
id: 3,
|
||||
type: "消防车",
|
||||
creator: "系统管理员",
|
||||
createTime: "2024-08-17 15:48:30",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
tab: "普通车",
|
||||
list: [
|
||||
{
|
||||
id: 4,
|
||||
type: "大巴车",
|
||||
creator: "系统管理员",
|
||||
createTime: "2024-08-17 15:48:30",
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
const isTabNameExist = computed(() => {
|
||||
return editableTabs.value.some(
|
||||
(tab) => tab.label === newTabName.value.trim()
|
||||
);
|
||||
});
|
||||
|
||||
const tableData = ref([]);
|
||||
const handleTabsEdit = (targetName, action) => {
|
||||
if (action === "add") {
|
||||
const newTabName = `${++tabIndex}`;
|
||||
editableTabs.value.push({
|
||||
label: "新标签页",
|
||||
name: newTabName,
|
||||
content:''
|
||||
});
|
||||
editableTabsValue.value = newTabName;
|
||||
} else if (action === "remove") {
|
||||
const tabs = editableTabs.value;
|
||||
let activeName = editableTabsValue.value;
|
||||
if (activeName === targetName) {
|
||||
tabs.forEach((tab, index) => {
|
||||
if (tab.name === targetName) {
|
||||
const nextTab = tabs[index + 1] || tabs[index - 1];
|
||||
if (nextTab) {
|
||||
activeName = nextTab.name;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
const addTab = () => {
|
||||
if (!newTabName.value.trim()) return;
|
||||
const name = newTabName.value.trim();
|
||||
|
||||
editableTabsValue.value = activeName;
|
||||
editableTabs.value = tabs.filter((tab) => tab.name !== targetName);
|
||||
// 检查类型是否已存在
|
||||
if (isTabNameExist.value) {
|
||||
proxy.$modal.msgError("该车辆类型已存在");
|
||||
return;
|
||||
}
|
||||
|
||||
// 构造一级类型数据
|
||||
const submitData = {
|
||||
typeName: name,
|
||||
level: 1,
|
||||
parentId: null,
|
||||
};
|
||||
|
||||
// 调用接口添加一级类型
|
||||
addVehicle_type(submitData)
|
||||
.then((response) => {
|
||||
proxy.$modal.msgSuccess("新增类型成功");
|
||||
|
||||
// 重新获取一级类型列表
|
||||
getFirstTypeList();
|
||||
|
||||
// 切换到新添加的标签
|
||||
editableTabsValue.value = name;
|
||||
|
||||
// 清空输入框并关闭弹窗
|
||||
newTabName.value = "";
|
||||
showAddTabDialog.value = false;
|
||||
|
||||
// 获取新标签下的数据
|
||||
setTimeout(() => {
|
||||
getList();
|
||||
}, 100);
|
||||
})
|
||||
.catch(() => {
|
||||
// 添加失败时不关闭弹窗,让用户可以修改后重试
|
||||
});
|
||||
};
|
||||
// 搜索
|
||||
const handleSearch=()=>{
|
||||
|
||||
}
|
||||
// 重置
|
||||
const handleReset=()=>{
|
||||
const deleteCustomTab = (tabName) => {
|
||||
// 查找要删除的标签对应的typeId
|
||||
const firstType = firstTypeList.value.find((item) => item.value === tabName);
|
||||
if (!firstType || !firstType.typeId) return;
|
||||
|
||||
}
|
||||
// 新增
|
||||
const handleAdd=()=>{
|
||||
// 先检查该一级类型下是否有二级类型
|
||||
listVehicle_type({ firstType: tabName }).then((res) => {
|
||||
if (res.data && Array.isArray(res.data)) {
|
||||
// 过滤出当前选中tab对应的二级类型数据
|
||||
const currentTabData = res.data.filter(
|
||||
(item) =>
|
||||
item.level === 2 &&
|
||||
item.parentId &&
|
||||
res.data.some(
|
||||
(parent) =>
|
||||
parent.typeId === item.parentId && parent.typeName === tabName
|
||||
)
|
||||
);
|
||||
|
||||
}
|
||||
// 删除
|
||||
const handleDelete=(rows)=>{
|
||||
if (currentTabData.length > 0) {
|
||||
// 如果有二级类型,提示不能删除
|
||||
proxy.$modal.msgError("该二级类型已绑定车辆,不可删除");
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
// 表格
|
||||
const handleSelectionChange =()=>{
|
||||
// 如果没有二级类型,则可以删除
|
||||
proxy.$modal
|
||||
.confirm(`确定删除 ${tabName} 类型吗?`, "删除")
|
||||
.then(function () {
|
||||
return delVehicle_type(firstType.typeId);
|
||||
})
|
||||
.then(() => {
|
||||
proxy.$modal.msgSuccess("删除成功");
|
||||
|
||||
}
|
||||
// 编辑
|
||||
const handleEdit =(rows)=>{
|
||||
// 重新获取一级类型列表
|
||||
getFirstTypeList();
|
||||
|
||||
// 如果删除的是当前选中的标签,切换到第一个标签
|
||||
if (editableTabsValue.value === tabName) {
|
||||
// 这里不需要手动设置editableTabsValue,因为getFirstTypeList会处理
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// 删除失败时不做处理
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleTabClick = (tab) => {
|
||||
editableTabsValue.value = tab.paneName;
|
||||
queryParams.firstType = tab.paneName;
|
||||
// 不需要在这里调用getList,因为watch会触发调用
|
||||
// getList();
|
||||
};
|
||||
|
||||
// 列表相关
|
||||
const loading = ref(false);
|
||||
const vehicleTypeList = ref([]);
|
||||
const total = ref(0);
|
||||
const ids = ref([]);
|
||||
const single = ref(true);
|
||||
const multiple = ref(true);
|
||||
const title = ref("");
|
||||
const open = ref(false);
|
||||
|
||||
// 查询参数
|
||||
const queryParams = ref({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
firstType: "无人车",
|
||||
typeName: undefined,
|
||||
});
|
||||
|
||||
// 表单参数
|
||||
const form = ref({});
|
||||
const rules = ref({
|
||||
firstType: [
|
||||
{ required: true, message: "一级类型不能为空", trigger: "change" },
|
||||
],
|
||||
typeName: [{ required: true, message: "二级类型不能为空", trigger: "blur" }],
|
||||
});
|
||||
|
||||
const firstTypeList = ref([
|
||||
{ label: "无人车", value: "无人车" },
|
||||
{ label: "特勤车", value: "特勤车" },
|
||||
{ label: "普通车", value: "普通车" },
|
||||
]);
|
||||
|
||||
/** 查询车辆类型列表 */
|
||||
function getList() {
|
||||
loading.value = true;
|
||||
queryParams.value.firstType = editableTabsValue.value;
|
||||
listVehicle_type(queryParams.value)
|
||||
.then((res) => {
|
||||
loading.value = false;
|
||||
// 根据接口返回的数据结构进行过滤,只显示当前选中tab对应的二级类型数据
|
||||
if (res.data && Array.isArray(res.data)) {
|
||||
// 过滤出当前选中tab对应的二级类型数据
|
||||
const currentTabData = res.data.filter(
|
||||
(item) =>
|
||||
item.level === 2 &&
|
||||
item.parentId &&
|
||||
res.data.some(
|
||||
(parent) =>
|
||||
parent.typeId === item.parentId &&
|
||||
parent.typeName === editableTabsValue.value
|
||||
)
|
||||
);
|
||||
|
||||
// 转换数据结构以适应表格显示
|
||||
vehicleTypeList.value = currentTabData.map((item) => ({
|
||||
typeId: item.typeId,
|
||||
firstType: editableTabsValue.value,
|
||||
typeName: item.typeName,
|
||||
createBy: item.createBy || "系统管理员",
|
||||
createTime: item.createTime,
|
||||
}));
|
||||
|
||||
total.value = vehicleTypeList.value.length;
|
||||
} else {
|
||||
vehicleTypeList.value = [];
|
||||
total.value = 0;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
vehicleTypeList.value = [];
|
||||
total.value = 0;
|
||||
});
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery() {
|
||||
queryParams.value.pageNum = 1;
|
||||
getList();
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function resetQuery() {
|
||||
queryParams.value.typeName = undefined;
|
||||
queryParams.value.pageNum = 1;
|
||||
handleQuery();
|
||||
}
|
||||
|
||||
/** 选择条数 */
|
||||
function handleSelectionChange(selection) {
|
||||
ids.value = selection.map((item) => item.typeId);
|
||||
single.value = selection.length != 1;
|
||||
multiple.value = !selection.length;
|
||||
}
|
||||
|
||||
/** 新增按钮操作 */
|
||||
function handleAdd() {
|
||||
reset();
|
||||
form.value.firstType = editableTabsValue.value;
|
||||
open.value = true;
|
||||
title.value = "添加车辆类型";
|
||||
}
|
||||
|
||||
/** 修改按钮操作 */
|
||||
function handleUpdate(row) {
|
||||
reset();
|
||||
const typeId = row.typeId || ids.value[0];
|
||||
getVehicle_type(typeId).then((response) => {
|
||||
form.value = response.data || {};
|
||||
open.value = true;
|
||||
title.value = "修改车辆类型";
|
||||
});
|
||||
}
|
||||
|
||||
/** 提交表单 */
|
||||
function submitForm() {
|
||||
proxy.$refs["vehicleTypeRef"].validate((valid) => {
|
||||
if (valid) {
|
||||
// 根据接口要求调整提交的数据结构
|
||||
const submitData = {
|
||||
typeId: form.value.typeId,
|
||||
typeName: form.value.typeName,
|
||||
parentId: null,
|
||||
level: 2,
|
||||
};
|
||||
|
||||
// 查找一级类型对应的typeId
|
||||
if (form.value.firstType) {
|
||||
const firstType = firstTypeList.value.find(
|
||||
(item) => item.value === form.value.firstType
|
||||
);
|
||||
if (firstType && firstType.typeId) {
|
||||
submitData.parentId = firstType.typeId;
|
||||
}
|
||||
}
|
||||
|
||||
if (form.value.typeId != undefined) {
|
||||
updateVehicle_type(submitData).then((response) => {
|
||||
proxy.$modal.msgSuccess("修改成功");
|
||||
open.value = false;
|
||||
getList();
|
||||
});
|
||||
} else {
|
||||
addVehicle_type(submitData).then((response) => {
|
||||
proxy.$modal.msgSuccess("新增成功");
|
||||
open.value = false;
|
||||
getList();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** 删除按钮操作 */
|
||||
function handleDelete(row) {
|
||||
const typeIds = row.typeId || ids.value;
|
||||
proxy.$modal
|
||||
.confirm("确定删除此车辆类型吗?", "删除")
|
||||
.then(function () {
|
||||
return delVehicle_type(typeIds);
|
||||
})
|
||||
.then(() => {
|
||||
getList();
|
||||
proxy.$modal.msgSuccess("删除成功");
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
/** 批量删除按钮操作 */
|
||||
function handleBatchDelete() {
|
||||
if (ids.value.length === 0) {
|
||||
proxy.$modal.msgError("请选择要删除的数据");
|
||||
return;
|
||||
}
|
||||
proxy.$modal
|
||||
.confirm("确认删除选中的数据项?")
|
||||
.then(function () {
|
||||
return delVehicle_type(ids.value);
|
||||
})
|
||||
.then(() => {
|
||||
getList();
|
||||
proxy.$modal.msgSuccess("删除成功");
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
/** 重置表单 */
|
||||
function reset() {
|
||||
form.value = {
|
||||
typeId: undefined,
|
||||
firstType: editableTabsValue.value,
|
||||
typeName: undefined,
|
||||
};
|
||||
proxy.resetForm("vehicleTypeRef");
|
||||
}
|
||||
|
||||
/** 取消按钮 */
|
||||
function cancel() {
|
||||
open.value = false;
|
||||
reset();
|
||||
}
|
||||
|
||||
// 初始化时获取一级类型数据
|
||||
function getFirstTypeList() {
|
||||
listVehicle_type({ level: 1 }).then((res) => {
|
||||
if (res.data && Array.isArray(res.data)) {
|
||||
// 过滤出一级类型数据
|
||||
const firstTypes = res.data.filter((item) => item.level === 1);
|
||||
|
||||
// 更新firstTypeList,保留typeId信息
|
||||
firstTypeList.value = firstTypes.map((item) => ({
|
||||
label: item.typeName,
|
||||
value: item.typeName,
|
||||
typeId: item.typeId,
|
||||
}));
|
||||
|
||||
// 更新可编辑的tab列表
|
||||
editableTabs.value = firstTypes.map((item) => ({
|
||||
label: item.typeName,
|
||||
name: item.typeName,
|
||||
content: "",
|
||||
}));
|
||||
|
||||
// 如果当前选中的tab不在列表中,则选择第一个
|
||||
if (
|
||||
!editableTabs.value.some(
|
||||
(tab) => tab.name === editableTabsValue.value
|
||||
) &&
|
||||
editableTabs.value.length > 0
|
||||
) {
|
||||
editableTabsValue.value = editableTabs.value[0].name;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
getFirstTypeList();
|
||||
getList();
|
||||
});
|
||||
|
||||
watch(editableTabsValue, (newValue) => {
|
||||
if (newValue) {
|
||||
queryParams.value.firstType = newValue;
|
||||
getList();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@ -209,20 +546,76 @@ const handleEdit =(rows)=>{
|
||||
background-color: #292c38;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.demo-tabs {
|
||||
|
||||
.tabs-container {
|
||||
position: relative;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.demo-tabs {
|
||||
margin-bottom: 0;
|
||||
flex: 1;
|
||||
:deep(.el-tabs__header) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
:deep(.el-tabs__item){
|
||||
color:#fff!important;
|
||||
:deep(.el-tabs__item) {
|
||||
color: #fff !important;
|
||||
}
|
||||
:deep(.el-tabs__active-bar) {
|
||||
// background-color: #4C4F5F !important;
|
||||
}
|
||||
:deep(.el-tabs__nav-wrap::after) {
|
||||
height: 1px;
|
||||
background-color: #4c4f5f;
|
||||
}
|
||||
:deep(.el-tabs__item.is-active) {
|
||||
color: #347ae2 !important;
|
||||
}
|
||||
:deep(.el-tabs__item:hover) {
|
||||
color: #347ae2 !important;
|
||||
}
|
||||
:deep(.tab-label-text) {
|
||||
margin-right: 4px;
|
||||
}
|
||||
:deep(.tab-delete-icon) {
|
||||
opacity: 0;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.3s ease;
|
||||
margin-left: 4px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.delete-icon-img {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
:deep(.el-tabs__item.is-active .tab-delete-icon) {
|
||||
opacity: 1;
|
||||
}
|
||||
:deep(.el-tabs__item:hover .tab-delete-icon) {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.add-tab-btn {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 10px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-left: 8px;
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
transition: filter 0.2s;
|
||||
|
||||
@ -1,52 +1,62 @@
|
||||
<template>
|
||||
<div class="login">
|
||||
<h1 class="title">青岛机场无人驾驶车辆协同云平台</h1>
|
||||
<el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form">
|
||||
<h3 class="title">青岛机场无人驾驶车辆协同云平台</h3>
|
||||
<div class="login-title">登录</div>
|
||||
|
||||
<div class="form-label">账号</div>
|
||||
<el-form-item prop="username">
|
||||
<el-input
|
||||
v-model="loginForm.username"
|
||||
type="text"
|
||||
size="large"
|
||||
auto-complete="off"
|
||||
placeholder="账号"
|
||||
placeholder="请输入账号"
|
||||
>
|
||||
<template #prefix><svg-icon icon-class="user" class="el-input__icon input-icon" /></template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<div class="form-label">密码</div>
|
||||
<el-form-item prop="password">
|
||||
<el-input
|
||||
v-model="loginForm.password"
|
||||
type="password"
|
||||
size="large"
|
||||
auto-complete="off"
|
||||
placeholder="密码"
|
||||
placeholder="请输入密码"
|
||||
@keyup.enter="handleLogin"
|
||||
>
|
||||
<template #prefix><svg-icon icon-class="password" class="el-input__icon input-icon" /></template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<div class="form-label" v-if="captchaEnabled">验证码</div>
|
||||
<el-form-item prop="code" v-if="captchaEnabled">
|
||||
<el-input
|
||||
v-model="loginForm.code"
|
||||
size="large"
|
||||
auto-complete="off"
|
||||
placeholder="验证码"
|
||||
style="width: 63%"
|
||||
@keyup.enter="handleLogin"
|
||||
>
|
||||
<template #prefix><svg-icon icon-class="validCode" class="el-input__icon input-icon" /></template>
|
||||
</el-input>
|
||||
<div class="login-code">
|
||||
<img :src="codeUrl" @click="getCode" class="login-code-img"/>
|
||||
<div class="captcha-container">
|
||||
<el-input
|
||||
v-model="loginForm.code"
|
||||
size="large"
|
||||
auto-complete="off"
|
||||
placeholder="请输入验证码"
|
||||
@keyup.enter="handleLogin"
|
||||
>
|
||||
<template #prefix><svg-icon icon-class="validCode" class="el-input__icon input-icon" /></template>
|
||||
</el-input>
|
||||
<div class="login-code">
|
||||
<img :src="codeUrl" @click="getCode" class="login-code-img"/>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-checkbox v-model="loginForm.rememberMe" style="margin:0px 0px 25px 0px;">记住密码</el-checkbox>
|
||||
|
||||
<el-checkbox v-model="loginForm.rememberMe" class="remember-me">记住密码</el-checkbox>
|
||||
|
||||
<el-form-item style="width:100%;">
|
||||
<el-button
|
||||
:loading="loading"
|
||||
size="large"
|
||||
type="primary"
|
||||
style="width:100%;"
|
||||
class="login-button"
|
||||
@click.prevent="handleLogin"
|
||||
>
|
||||
<span v-if="!loading">登 录</span>
|
||||
@ -151,72 +161,126 @@ getCookie();
|
||||
<style lang='scss' scoped>
|
||||
.login {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
background-image: url("../assets/images/login-background.png");
|
||||
background-size: 100% 100%;
|
||||
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0px auto 30px auto;
|
||||
text-align: center;
|
||||
color: #ffffff;
|
||||
font-size: 20px;
|
||||
font-size: 48px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
color: #fff;
|
||||
font-size: 36px;
|
||||
font-weight: normal;
|
||||
margin-bottom: 30px;
|
||||
display: inline-block;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.form-label {
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
margin-bottom: 10px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
border-radius: 8px;
|
||||
background: rgba(41, 44, 56, 0.8);
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
|
||||
width: 400px;
|
||||
padding: 25px 25px 5px 25px;
|
||||
width: 636px;
|
||||
padding: 40px 60px;
|
||||
|
||||
.el-form-item {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.el-input {
|
||||
height: 40px;
|
||||
height: 56px;
|
||||
|
||||
:deep(.el-input__wrapper) {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
box-shadow: none;
|
||||
border-radius: 4px;
|
||||
padding: 0 15px;
|
||||
height: 56px !important;
|
||||
line-height: 56px;
|
||||
}
|
||||
|
||||
:deep(.el-input__inner) {
|
||||
color: #ffffff;
|
||||
height: 40px;
|
||||
height: 56px;
|
||||
line-height: 56px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
input {
|
||||
height: 40px;
|
||||
height: 56px;
|
||||
}
|
||||
}
|
||||
|
||||
.input-icon {
|
||||
height: 39px;
|
||||
width: 14px;
|
||||
margin-left: 0px;
|
||||
height: 56px;
|
||||
width: 20px;
|
||||
margin-right: 10px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
}
|
||||
.login-tip {
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
color: #bfbfbf;
|
||||
|
||||
.captcha-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
justify-content: space-between;
|
||||
|
||||
.el-input {
|
||||
flex: 1;
|
||||
// width: calc(65% - 5px);
|
||||
|
||||
:deep(.el-input__wrapper) {
|
||||
height: 56px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.login-code {
|
||||
width: 33%;
|
||||
height: 40px;
|
||||
float: right;
|
||||
// width: calc(35% - 5px);
|
||||
height: 56px;
|
||||
img {
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
height: 56px;
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
.el-login-footer {
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
|
||||
.remember-me {
|
||||
color: #fff;
|
||||
font-family: Arial;
|
||||
font-size: 12px;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
.login-code-img {
|
||||
height: 40px;
|
||||
padding-left: 12px;
|
||||
|
||||
.login-button {
|
||||
height: 56px;
|
||||
font-size: 18px;
|
||||
background-color: #347AE2 !important;
|
||||
border-color: #347AE2 !important;
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.link-type {
|
||||
color: #347AE2;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@ -1,7 +1,19 @@
|
||||
<template>
|
||||
<div class="platform-overview platform-no-padding">
|
||||
<OpenLayersMap ref="mapRef" />
|
||||
<OpenLayersMap
|
||||
ref="mapRef"
|
||||
:vehicleMovementControl="vehicleMovementRef"
|
||||
:vehicleCategories="vehicleCategories"
|
||||
@setCategoryVisibility="handleSetCategoryVisibility"
|
||||
/>
|
||||
<CarAlarm />
|
||||
<!-- 报警通知面板 -->
|
||||
<AlarmNotification v-if="showAlarmPanel" @close="showAlarmPanel = false" />
|
||||
<!-- 报警通知按钮 -->
|
||||
<div class="alarm-btn" @click="showAlarmPanel = !showAlarmPanel">
|
||||
<i class="alarm-icon"></i>
|
||||
<span class="alarm-badge" v-if="alarmCount > 0">{{ alarmCount }}</span>
|
||||
</div>
|
||||
<!-- 右侧展开/收起按钮 -->
|
||||
<img
|
||||
class="eventlist-toggle-btn"
|
||||
@ -12,6 +24,11 @@
|
||||
/>
|
||||
<!-- 车辆列表 -->
|
||||
<Eventlist v-if="showEventList" />
|
||||
|
||||
<!-- 车辆移动控制组件 -->
|
||||
<VehicleMovementControl :map="map" ref="vehicleMovementRef" v-if="map" />
|
||||
|
||||
|
||||
|
||||
<!-- 绘制工具栏 -->
|
||||
<!-- <div class="draw-toolbar">
|
||||
@ -24,19 +41,41 @@
|
||||
<button @click="importRouteData" class="toolbar-button import">导入数据</button>
|
||||
</div>
|
||||
</div>-->
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
import { ref, onMounted, onUnmounted, watch, computed } 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 AlarmNotification from '../../components/map/info/AlarmNotification.vue';
|
||||
import VehicleMovementControl from '../../components/map/controls/VehicleMovementControl.vue';
|
||||
import btn_left from '../../assets/images/btn_left.png';
|
||||
import btn_right from '../../assets/images/btn_right.png';
|
||||
|
||||
const showEventList = ref(false); // 默认不显示列表
|
||||
const showAlarmPanel = ref(false); // 默认不显示报警面板
|
||||
const alarmCount = ref(4); // 报警数量
|
||||
const mapRef = ref(null);
|
||||
const vehicleMovementRef = ref(null);
|
||||
const map = ref(null); // 存储地图实例
|
||||
|
||||
// 获取车辆分类数据,从VehicleMovementControl组件中获取
|
||||
const vehicleCategories = computed(() => {
|
||||
return vehicleMovementRef.value?.vehicleCategories || {};
|
||||
});
|
||||
|
||||
// 处理设置分类可见性
|
||||
function handleSetCategoryVisibility(type, settings) {
|
||||
if (vehicleMovementRef.value && vehicleMovementRef.value.setCategoryVisibility) {
|
||||
vehicleMovementRef.value.setCategoryVisibility(type, settings);
|
||||
}
|
||||
}
|
||||
|
||||
// 判断是否为开发环境
|
||||
const isDevelopment = process.env.NODE_ENV === 'development';
|
||||
|
||||
function toggleEventList() {
|
||||
showEventList.value = !showEventList.value;
|
||||
@ -83,12 +122,35 @@ function importRouteData() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
document.querySelector('.app-main')?.classList.add('platform-no-padding');
|
||||
|
||||
// 获取地图实例
|
||||
if (mapRef.value) {
|
||||
// 等待地图实例初始化完成
|
||||
setTimeout(() => {
|
||||
map.value = mapRef.value.map;
|
||||
console.log('地图实例已获取:', map.value);
|
||||
|
||||
// 可以在这里启动测试
|
||||
// testVehicleMovement();
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.querySelector('.app-main')?.classList.remove('platform-no-padding');
|
||||
});
|
||||
|
||||
// 监听地图实例引用变化
|
||||
watch(() => mapRef.value?.map, (newMap) => {
|
||||
if (newMap) {
|
||||
map.value = newMap;
|
||||
console.log('地图实例已更新:', map.value);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@ -102,6 +164,48 @@ onUnmounted(() => {
|
||||
z-index:1;
|
||||
}
|
||||
|
||||
/* 报警通知按钮 */
|
||||
.alarm-btn {
|
||||
position: absolute;
|
||||
left: 25px;
|
||||
top: 15%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background-color: rgba(53, 61, 72, 0.9);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
z-index: 1001;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.alarm-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23ffffff"><path d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.9 2 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z"/></svg>');
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
.alarm-badge {
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
right: -5px;
|
||||
background-color: #FF4D4F;
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 9px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
/* 右侧展开/收起按钮样式 */
|
||||
.eventlist-toggle-btn {
|
||||
position: absolute;
|
||||
@ -120,6 +224,31 @@ onUnmounted(() => {
|
||||
right: 405px;
|
||||
}
|
||||
|
||||
/* 测试按钮 - 仅开发阶段使用 */
|
||||
.test-controls {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
left: 20px;
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
.test-btn {
|
||||
padding: 8px 16px;
|
||||
background-color: rgba(33, 150, 243, 0.8);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.test-btn:hover {
|
||||
background-color: rgba(33, 150, 243, 1);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* 绘制工具栏 */
|
||||
.draw-toolbar {
|
||||
position: absolute;
|
||||
|
||||
@ -4,14 +4,14 @@
|
||||
<!-- 搜索区域 -->
|
||||
<div class="search-area">
|
||||
<el-input
|
||||
v-model="queryParams.driverName"
|
||||
v-model="queryParams.userName"
|
||||
placeholder="请输入驾驶员姓名"
|
||||
clearable
|
||||
prefix-icon="Search"
|
||||
class="search-input"
|
||||
/>
|
||||
|
||||
<el-select v-model="queryParams.roleId" placeholder="角色" clearable class="search-select">
|
||||
<!-- <el-select v-model="queryParams.roleId" placeholder="角色" clearable class="search-select">
|
||||
<el-option label="全部" value="" />
|
||||
<el-option
|
||||
v-for="role in roleOptions"
|
||||
@ -19,7 +19,7 @@
|
||||
:label="role.roleName"
|
||||
:value="role.roleId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-select> -->
|
||||
|
||||
<div class="search-buttons">
|
||||
<el-button type="primary" class="search-btn" @click="handleQuery">搜索</el-button>
|
||||
@ -28,7 +28,7 @@
|
||||
|
||||
<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="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>
|
||||
</div>
|
||||
@ -59,15 +59,14 @@
|
||||
<el-table-column label="人像" prop="avatar" align="left" >
|
||||
<template #default="scope">
|
||||
<div class="avatar-box">
|
||||
<img src="../../../assets/images/avatar.png" alt="" />
|
||||
<img :src="scope.row.avatar" alt="" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</el-table-column>
|
||||
<el-table-column label="姓名" prop="driverName" align="left" />
|
||||
<el-table-column label="角色" prop="driverName" align="left" />
|
||||
<el-table-column label="姓名" prop="userName" align="left" />
|
||||
|
||||
<el-table-column label="驾照类型" prop="licenseType" align="left" />
|
||||
<el-table-column label="手机号" prop="phonenumber" align="left" />
|
||||
<el-table-column label="账号状态" align="left">
|
||||
<template #default="scope">
|
||||
{{ scope.row.status === '0' ? '正常' : '停用' }}
|
||||
@ -95,25 +94,25 @@
|
||||
<el-form :model="form" :rules="rules" ref="driverRef" label-width="100px">
|
||||
<el-row>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="姓名" prop="driverName">
|
||||
<el-input
|
||||
v-model="form.driverName"
|
||||
placeholder="请输入驾驶员姓名"
|
||||
/>
|
||||
<el-form-item label="姓名" prop="userName">
|
||||
<el-select
|
||||
v-model="selectedUser"
|
||||
placeholder="请选择驾驶员姓名"
|
||||
:disabled="isEdit"
|
||||
class="full-width-select"
|
||||
@change="handleUserChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="user in userOptions"
|
||||
:key="user.userId"
|
||||
:label="user.userName"
|
||||
:value="user"
|
||||
/>
|
||||
</el-select>
|
||||
</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="驾驶证类型" prop="licenseType">
|
||||
@ -124,41 +123,15 @@
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="角色">
|
||||
<el-radio-group v-model="form.roleId">
|
||||
<el-radio
|
||||
v-for="role in roleOptions"
|
||||
:key="role.roleId"
|
||||
:label="role.roleId.toString()">
|
||||
{{ role.roleName }}
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="状态">
|
||||
<el-radio-group v-model="form.status">
|
||||
<el-radio
|
||||
v-for="dict in sys_normal_disable"
|
||||
:key="dict.value"
|
||||
:label="dict.value"
|
||||
>{{ dict.label }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<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">
|
||||
<div class="avatar-replace" @click.stop="handleRemoveAvatar" v-if="!isEdit">
|
||||
<el-icon><Close /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
@ -170,6 +143,7 @@
|
||||
:headers="upload.headers"
|
||||
:before-upload="beforeAvatarUpload"
|
||||
:on-success="handleAvatarSuccess"
|
||||
:disabled="isEdit"
|
||||
>
|
||||
<div class="avatar-upload-placeholder">
|
||||
<el-icon class="avatar-uploader-icon"><Plus /></el-icon>
|
||||
@ -179,7 +153,7 @@
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-row> -->
|
||||
|
||||
</el-form>
|
||||
<template #footer>
|
||||
@ -238,15 +212,17 @@
|
||||
import { getToken } from "@/utils/auth";
|
||||
import { parseTime } from "@/utils/ruoyi";
|
||||
import { listRole } from "@/api/system/role";
|
||||
import { getInitPassword } from "@/api/system/user";
|
||||
// import { getInitPassword } from "@/utils/auth";
|
||||
import { listUser } from "@/api/system/user";
|
||||
import { getRoleUsers } from "@/api/system/role";
|
||||
import {
|
||||
listDriver,
|
||||
getDriver,
|
||||
addDriver,
|
||||
updateDriver,
|
||||
delDriver,
|
||||
uploadAvatar
|
||||
} from "@/api/system/driver";
|
||||
listDriver_info,
|
||||
getDriver_info,
|
||||
addDriver_info,
|
||||
updateDriver_info,
|
||||
delDriver_info,
|
||||
|
||||
} from "@/api/system/driver_info.js"; // uploadAvatar
|
||||
import { Plus, Delete, UploadFilled, Close } from '@element-plus/icons-vue';
|
||||
|
||||
const { proxy } = getCurrentInstance();
|
||||
@ -261,68 +237,9 @@ const multiple = ref(true);
|
||||
const total = ref(0);
|
||||
const title = ref("");
|
||||
const roleOptions = ref([]);
|
||||
const initPassword = ref("");
|
||||
|
||||
// 模拟数据
|
||||
const mockDriverData = [
|
||||
{
|
||||
driverId: 1,
|
||||
driverName: '张三',
|
||||
idCard: '110101199001011234',
|
||||
licenseType: 'A1',
|
||||
phonenumber: '13800138001',
|
||||
status: '0',
|
||||
createTime: '2023-01-01 08:00:00',
|
||||
avatar: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'
|
||||
},
|
||||
{
|
||||
driverId: 2,
|
||||
driverName: '李四',
|
||||
idCard: '110101199001021235',
|
||||
licenseType: 'B1',
|
||||
phonenumber: '13800138002',
|
||||
status: '0',
|
||||
createTime: '2023-01-02 08:00:00',
|
||||
avatar: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'
|
||||
},
|
||||
{
|
||||
driverId: 3,
|
||||
driverName: '王五',
|
||||
idCard: '110101199001031236',
|
||||
licenseType: 'A2',
|
||||
phonenumber: '13800138003',
|
||||
status: '1',
|
||||
createTime: '2023-01-03 08:00:00',
|
||||
avatar: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'
|
||||
},
|
||||
{
|
||||
driverId: 4,
|
||||
driverName: '赵六',
|
||||
idCard: '110101199001041237',
|
||||
licenseType: 'C1',
|
||||
phonenumber: '13800138004',
|
||||
status: '0',
|
||||
createTime: '2023-01-04 08:00:00',
|
||||
avatar: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'
|
||||
},
|
||||
{
|
||||
driverId: 5,
|
||||
driverName: '钱七',
|
||||
idCard: '110101199001051238',
|
||||
licenseType: 'B2',
|
||||
phonenumber: '13800138005',
|
||||
status: '0',
|
||||
createTime: '2023-01-05 08:00:00',
|
||||
avatar: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'
|
||||
}
|
||||
];
|
||||
|
||||
// 模拟角色数据
|
||||
const mockRoleData = [
|
||||
{ roleId: 1, roleName: '超级管理员' },
|
||||
{ roleId: 2, roleName: '普通用户' },
|
||||
{ roleId: 3, roleName: '驾驶员' }
|
||||
];
|
||||
const userOptions = ref([]);
|
||||
const isEdit = ref(false);
|
||||
const selectedUser = ref(null);
|
||||
|
||||
// 驾驶员导入参数
|
||||
const upload = reactive({
|
||||
@ -331,51 +248,50 @@ const upload = reactive({
|
||||
isUploading: false,
|
||||
updateSupport: 0,
|
||||
headers: { Authorization: "Bearer " + getToken() },
|
||||
url: import.meta.env.VITE_APP_BASE_API + "/system/driver/importData",
|
||||
avatarUrl: import.meta.env.VITE_APP_BASE_API + "/system/driver/avatar"
|
||||
url: import.meta.env.VITE_APP_BASE_API + "/system/driver_info/importData",
|
||||
avatarUrl: import.meta.env.VITE_APP_BASE_API + "/system/driver_info/avatar"
|
||||
});
|
||||
|
||||
const queryParams = ref({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
driverName: undefined,
|
||||
phonenumber: undefined,
|
||||
userName: undefined,
|
||||
status: undefined,
|
||||
roleId: undefined
|
||||
userId: undefined
|
||||
});
|
||||
|
||||
const form = ref({});
|
||||
|
||||
const rules = ref({
|
||||
driverName: [
|
||||
{ required: true, message: "驾驶员姓名不能为空", trigger: "blur" }
|
||||
],
|
||||
idCard: [
|
||||
{ required: true, message: "身份证号不能为空", trigger: "blur" },
|
||||
{ pattern: /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/, 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" }
|
||||
],
|
||||
licenseType: [
|
||||
{ required: true, message: "驾照类型不能为空", trigger: "change" }
|
||||
]
|
||||
});
|
||||
// const rules = ref({
|
||||
// userName: [
|
||||
// { required: true, message: "驾驶员姓名不能为空", trigger: "change" }
|
||||
// ],
|
||||
// licenseType: [
|
||||
// { required: true, message: "驾照类型不能为空", trigger: "change" }
|
||||
// ],
|
||||
// userId: [
|
||||
// { required: true, message: "用户ID不能为空", trigger: "change" }
|
||||
// ]
|
||||
// });
|
||||
|
||||
/** 处理用户选择变更 */
|
||||
function handleUserChange(value) {
|
||||
console.log("value",value);
|
||||
if (value) {
|
||||
form.value.userId = value.userId;
|
||||
form.value.userName = value.userName;
|
||||
form.value.avatar = value.avatar;
|
||||
} else {
|
||||
form.value.userId = undefined;
|
||||
form.value.userName = undefined;
|
||||
form.value.avatar = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询驾驶员列表 */
|
||||
function getList() {
|
||||
loading.value = true;
|
||||
// 使用模拟数据
|
||||
setTimeout(() => {
|
||||
loading.value = false;
|
||||
driverList.value = mockDriverData;
|
||||
total.value = mockDriverData.length;
|
||||
}, 300);
|
||||
|
||||
// API调用代码保留,用于后续替换
|
||||
/*
|
||||
listDriver(queryParams.value).then(res => {
|
||||
listDriver_info(queryParams.value).then(res => {
|
||||
loading.value = false;
|
||||
driverList.value = res.rows || [];
|
||||
total.value = res.total || 0;
|
||||
@ -384,7 +300,15 @@ function getList() {
|
||||
driverList.value = [];
|
||||
total.value = 0;
|
||||
});
|
||||
*/
|
||||
}
|
||||
// getUserOptions();
|
||||
/** 获取用户列表 */
|
||||
function getUserOptions() {
|
||||
|
||||
getRoleUsers(3).then(res => {
|
||||
userOptions.value = res.rows;
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
@ -402,91 +326,59 @@ function resetQuery() {
|
||||
|
||||
/** 选择条数 */
|
||||
function handleSelectionChange(selection) {
|
||||
ids.value = selection.map(item => item.driverId);
|
||||
ids.value = selection.map(item => item.userId);
|
||||
single.value = selection.length != 1;
|
||||
multiple.value = !selection.length;
|
||||
}
|
||||
|
||||
/** 删除按钮操作 */
|
||||
function handleDelete(row) {
|
||||
const driverIds = row.driverId || ids.value;
|
||||
const userId = row.userId || ids.value;
|
||||
proxy.$modal.confirm('确定删除此驾驶员吗?', '删除').then(function() {
|
||||
return delDriver(driverIds);
|
||||
return delDriver_info(userId);
|
||||
}).then(() => {
|
||||
getList();
|
||||
proxy.$modal.msgSuccess("删除成功");
|
||||
}).catch(() => {
|
||||
// 模拟删除成功
|
||||
getList();
|
||||
});
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
/** 新增按钮操作 */
|
||||
function handleAdd() {
|
||||
reset();
|
||||
// 使用模拟密码
|
||||
initPassword.value = "123456";
|
||||
title.value = "添加驾驶员";
|
||||
isEdit.value = false;
|
||||
selectedUser.value = null;
|
||||
title.value = "添加驾驶员信息";
|
||||
open.value = true;
|
||||
|
||||
// API调用代码保留,用于后续替换
|
||||
/*
|
||||
getInitPassword().then(response => {
|
||||
if (response && response.data) {
|
||||
initPassword.value = response.data.password;
|
||||
}
|
||||
title.value = "添加驾驶员";
|
||||
open.value = true;
|
||||
}).catch(() => {
|
||||
title.value = "添加驾驶员";
|
||||
open.value = true;
|
||||
});
|
||||
*/
|
||||
}
|
||||
|
||||
/** 修改按钮操作 */
|
||||
function handleUpdate(row) {
|
||||
reset();
|
||||
const driverId = row.driverId || ids.value[0];
|
||||
// 使用模拟数据
|
||||
const driverInfo = mockDriverData.find(d => d.driverId === driverId);
|
||||
if (driverInfo) {
|
||||
form.value = {...driverInfo};
|
||||
open.value = true;
|
||||
title.value = "修改驾驶员";
|
||||
}
|
||||
|
||||
// API调用代码保留,用于后续替换
|
||||
/*
|
||||
getDriver(driverId).then(response => {
|
||||
isEdit.value = true;
|
||||
const userId = row.userId || ids.value[0];
|
||||
getDriver_info(userId).then(response => {
|
||||
form.value = response.data || {};
|
||||
form.value.roleId = response.roleIds && response.roleIds.length > 0 ? response.roleIds[0].toString() : "2";
|
||||
// 确保表单中有正确的ID字段
|
||||
form.value.driverId = form.value.driverId || form.value.userId;
|
||||
// 找到对应的用户并设置selectedUser
|
||||
if (form.value.userId) {
|
||||
selectedUser.value = userOptions.value.find(user => user.userId === form.value.userId) || null;
|
||||
}
|
||||
open.value = true;
|
||||
title.value = "修改驾驶员";
|
||||
}).catch(() => {
|
||||
// 如果API请求失败,使用模拟方式
|
||||
const driverInfo = driverList.value.find(d => d.driverId === driverId);
|
||||
if (driverInfo) {
|
||||
form.value = {...driverInfo};
|
||||
open.value = true;
|
||||
title.value = "修改驾驶员";
|
||||
}
|
||||
});
|
||||
*/
|
||||
}
|
||||
|
||||
/** 重置表单 */
|
||||
function reset() {
|
||||
form.value = {
|
||||
driverId: undefined,
|
||||
driverName: undefined,
|
||||
idCard: undefined,
|
||||
phonenumber: undefined,
|
||||
userId: undefined,
|
||||
userName: undefined,
|
||||
licenseType: undefined,
|
||||
status: "0",
|
||||
roleId: "2", // 默认选择普通用户
|
||||
avatar: undefined
|
||||
avatar: undefined,
|
||||
driverId: undefined // 确保重置driverId
|
||||
};
|
||||
selectedUser.value = null;
|
||||
proxy.resetForm("driverRef");
|
||||
}
|
||||
|
||||
@ -494,60 +386,31 @@ function reset() {
|
||||
function submitForm() {
|
||||
proxy.$refs["driverRef"].validate(valid => {
|
||||
if (valid) {
|
||||
// 转换roleId为roleIds数组
|
||||
if (form.value.roleId) {
|
||||
form.value.roleIds = [form.value.roleId];
|
||||
// 确保表单中有用户ID
|
||||
if (!form.value.userId && selectedUser.value) {
|
||||
form.value.userId = selectedUser.value.userId;
|
||||
}
|
||||
|
||||
if (form.value.driverId != undefined) {
|
||||
// 模拟修改操作
|
||||
const index = mockDriverData.findIndex(item => item.driverId === form.value.driverId);
|
||||
if (index !== -1) {
|
||||
mockDriverData[index] = {...form.value};
|
||||
}
|
||||
proxy.$modal.msgSuccess("修改成功");
|
||||
open.value = false;
|
||||
getList();
|
||||
|
||||
// API调用代码保留,用于后续替换
|
||||
/*
|
||||
updateDriver(form.value).then(response => {
|
||||
proxy.$modal.msgSuccess("修改成功");
|
||||
open.value = false;
|
||||
getList();
|
||||
}).catch(() => {
|
||||
// 模拟成功
|
||||
if (form.value.driverId) {
|
||||
// 修改操作
|
||||
updateDriver_info(form.value).then(response => {
|
||||
proxy.$modal.msgSuccess("修改成功");
|
||||
open.value = false;
|
||||
getList();
|
||||
}).catch(error => {
|
||||
console.error("更新失败:", error);
|
||||
proxy.$modal.msgError("修改失败: " + (error.message || "未知错误"));
|
||||
});
|
||||
*/
|
||||
} else {
|
||||
// 模拟新增操作
|
||||
form.value.driverId = mockDriverData.length + 1;
|
||||
form.value.createTime = new Date().toISOString().replace('T', ' ').substring(0, 19);
|
||||
if (!form.value.avatar) {
|
||||
form.value.avatar = 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png';
|
||||
}
|
||||
mockDriverData.push({...form.value});
|
||||
proxy.$modal.msgSuccess("新增成功");
|
||||
open.value = false;
|
||||
getList();
|
||||
|
||||
// API调用代码保留,用于后续替换
|
||||
/*
|
||||
form.value.password = form.value.password || initPassword.value;
|
||||
addDriver(form.value).then(response => {
|
||||
proxy.$modal.msgSuccess("新增成功");
|
||||
open.value = false;
|
||||
getList();
|
||||
}).catch(() => {
|
||||
// 模拟成功
|
||||
// 新增操作
|
||||
addDriver_info(form.value).then(response => {
|
||||
proxy.$modal.msgSuccess("新增成功");
|
||||
open.value = false;
|
||||
getList();
|
||||
}).catch(error => {
|
||||
console.error("新增失败:", error);
|
||||
proxy.$modal.msgError("新增失败: " + (error.message || "未知错误"));
|
||||
});
|
||||
*/
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -561,17 +424,11 @@ function cancel() {
|
||||
|
||||
/** 导出按钮操作 */
|
||||
function handleExport() {
|
||||
// 模拟导出操作
|
||||
proxy.$modal.msgSuccess("导出功能已模拟");
|
||||
|
||||
// API调用代码保留,用于后续替换
|
||||
/*
|
||||
proxy.download("system/driver/export", {
|
||||
proxy.download("system/driver_info/export", {
|
||||
...queryParams.value,
|
||||
pageNum: undefined,
|
||||
pageSize: undefined
|
||||
}, `驾驶员数据_${new Date().getTime()}.xlsx`);
|
||||
*/
|
||||
}
|
||||
|
||||
/** 导入按钮操作 */
|
||||
@ -590,31 +447,13 @@ function handleFileSuccess(response, file, fileList) {
|
||||
upload.open = false;
|
||||
upload.isUploading = false;
|
||||
proxy.$refs["uploadRef"].clearFiles();
|
||||
|
||||
// 模拟导入成功
|
||||
proxy.$modal.msgSuccess("导入成功");
|
||||
mockDriverData.push({
|
||||
driverId: mockDriverData.length + 1,
|
||||
driverName: '导入用户',
|
||||
idCard: '110101199001061239',
|
||||
licenseType: 'C1',
|
||||
phonenumber: '13800138006',
|
||||
status: '0',
|
||||
createTime: new Date().toISOString().replace('T', ' ').substring(0, 19),
|
||||
avatar: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'
|
||||
});
|
||||
proxy.$modal.msgSuccess(response.msg);
|
||||
getList();
|
||||
}
|
||||
|
||||
/** 下载导入模板 */
|
||||
function importTemplate() {
|
||||
// 模拟下载模板操作
|
||||
proxy.$modal.msgSuccess("下载模板功能已模拟");
|
||||
|
||||
// API调用代码保留,用于后续替换
|
||||
/*
|
||||
proxy.download("system/driver/importTemplate", {}, `driver_template_${new Date().getTime()}.xlsx`);
|
||||
*/
|
||||
proxy.download("system/driver_info/importTemplate", {}, `driver_template_${new Date().getTime()}.xlsx`);
|
||||
}
|
||||
|
||||
/** 提交上传文件 */
|
||||
@ -644,9 +483,7 @@ function handleAvatarSuccess(res, file) {
|
||||
form.value.avatar = res.data.url;
|
||||
proxy.$modal.msgSuccess("上传成功");
|
||||
} else {
|
||||
proxy.$modal.msgError("上传失败");
|
||||
// 模拟上传成功
|
||||
form.value.avatar = URL.createObjectURL(file.raw);
|
||||
proxy.$modal.msgError(res.msg || "上传失败");
|
||||
}
|
||||
}
|
||||
|
||||
@ -657,20 +494,15 @@ function handleRemoveAvatar() {
|
||||
|
||||
// 获取角色下拉列表
|
||||
function getRoleOptions() {
|
||||
// 使用模拟角色数据
|
||||
roleOptions.value = mockRoleData;
|
||||
|
||||
// API调用代码保留,用于后续替换
|
||||
/*
|
||||
listRole().then(res => {
|
||||
roleOptions.value = res.rows || [];
|
||||
});
|
||||
*/
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getList();
|
||||
getRoleOptions();
|
||||
getUserOptions();
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -1233,4 +1065,21 @@ onMounted(() => {
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
// 下拉框充满整行的样式
|
||||
.full-width-select {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
:deep(.full-width-select) {
|
||||
width: 100% !important;
|
||||
|
||||
.el-input {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.el-input__wrapper {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -215,7 +215,7 @@ function getList() {
|
||||
function handleQuery() {
|
||||
queryParams.value.pageNum = 1;
|
||||
getList();
|
||||
}
|
||||
}
|
||||
/** 重置按钮操作 */
|
||||
function resetQuery() {
|
||||
dateRange.value = [];
|
||||
|
||||
@ -53,22 +53,28 @@
|
||||
</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="handleUpdate(scope.row)">编辑</el-button>
|
||||
<el-button link text type="primary" @click="handleDelete(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="人像" prop="avatar" align="left" class="avatar-box">
|
||||
<template #default="scope">
|
||||
<div class="avatar-box">
|
||||
<img :src="scope.row.avatar" alt="" />
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="用户名" prop="userName" align="left" />
|
||||
<!-- <el-table-column label="昵称" prop="userName" align="left" /> -->
|
||||
<el-table-column label="角色" align="left">
|
||||
<template #default="scope">
|
||||
<span>{{ scope.row.roles ? scope.row.roles.join(', ') : '-' }}</span>
|
||||
<span>{{ scope.row.roles ? scope.row.roles.map(role => role.roleName).join(', ') : '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="手机号" prop="phonenumber" align="left" />
|
||||
<el-table-column label="账号状态" align="left">
|
||||
<template #default="scope">
|
||||
|
||||
{{ scope.row.status === '0' ? '正常' : '停用' }}
|
||||
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" align="left" prop="createTime" width="160">
|
||||
@ -93,9 +99,9 @@
|
||||
<el-form :model="form" :rules="rules" ref="userRef" label-width="80px">
|
||||
<el-row>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="用户名" prop="nickName">
|
||||
<el-form-item label="用户名" prop="userName">
|
||||
<el-input
|
||||
v-model="form.nickName"
|
||||
v-model="form.userName"
|
||||
placeholder="请输入用户昵称"
|
||||
/>
|
||||
</el-form-item>
|
||||
@ -110,19 +116,47 @@
|
||||
</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-row>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="角色">
|
||||
<el-radio-group v-model="form.roleId">
|
||||
<el-radio
|
||||
<el-checkbox-group v-model="form.roleIds">
|
||||
<el-checkbox
|
||||
v-for="role in roleOptions"
|
||||
:key="role.roleId"
|
||||
:label="role.roleId.toString()">
|
||||
:label="role.roleId">
|
||||
{{ role.roleName }}
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
@ -205,9 +239,10 @@ import {
|
||||
updateUser,
|
||||
addUser,
|
||||
deptTreeSelect,
|
||||
getInitPassword
|
||||
uploadAvatar
|
||||
} from "@/api/system/user";
|
||||
import { listRole } from "@/api/system/role";
|
||||
import { Plus, Delete, UploadFilled, Close } from '@element-plus/icons-vue';
|
||||
|
||||
const router = useRouter();
|
||||
const { proxy } = getCurrentInstance();
|
||||
@ -234,7 +269,8 @@ const upload = reactive({
|
||||
isUploading: false,
|
||||
updateSupport: 0,
|
||||
headers: { Authorization: "Bearer " + getToken() },
|
||||
url: import.meta.env.VITE_APP_BASE_API + "/system/user/importData"
|
||||
url: import.meta.env.VITE_APP_BASE_API + "/system/user/importData",
|
||||
avatarUrl: import.meta.env.VITE_APP_BASE_API + "/system/user/profile/avatar"
|
||||
});
|
||||
|
||||
// 列显隐信息
|
||||
@ -265,7 +301,7 @@ const rules = ref({
|
||||
{ required: true, message: "用户名称不能为空", trigger: "blur" },
|
||||
{ min: 2, max: 20, message: "用户名称长度必须介于 2 和 20 之间", trigger: "blur" }
|
||||
],
|
||||
nickName: [{ required: true, message: "用户昵称不能为空", trigger: "blur" }],
|
||||
userName: [{ required: true, message: "用户昵称不能为空", trigger: "blur" }],
|
||||
password: [
|
||||
{ required: true, message: "用户密码不能为空", trigger: "blur" },
|
||||
{ min: 5, max: 20, message: "用户密码长度必须介于 5 和 20 之间", trigger: "blur" }
|
||||
@ -333,16 +369,16 @@ function handleBatchDelete() {
|
||||
/** 新增按钮操作 */
|
||||
function handleAdd() {
|
||||
reset();
|
||||
getInitPassword().then(response => {
|
||||
if (response && response.data) {
|
||||
initPassword.value = response.data.password;
|
||||
}
|
||||
// getInitPassword().then(response => {
|
||||
// if (response && response.data) {
|
||||
// initPassword.value = response.data.password;
|
||||
// }
|
||||
title.value = "添加用户";
|
||||
open.value = true;
|
||||
}).catch(() => {
|
||||
title.value = "添加用户";
|
||||
open.value = true;
|
||||
});
|
||||
// }).catch(() => {
|
||||
// title.value = "添加用户";
|
||||
// open.value = true;
|
||||
// });
|
||||
}
|
||||
|
||||
/** 修改按钮操作 */
|
||||
@ -351,8 +387,7 @@ function handleUpdate(row) {
|
||||
const userId = row.userId || ids.value[0];
|
||||
getUser(userId).then(response => {
|
||||
form.value = response.data;
|
||||
form.value.postIds = response.postIds;
|
||||
form.value.roleId = response.roleIds && response.roleIds.length > 0 ? response.roleIds[0].toString() : "2";
|
||||
form.value.roleIds = response.roleIds || [];
|
||||
open.value = true;
|
||||
title.value = "修改用户";
|
||||
form.password = "";
|
||||
@ -369,17 +404,17 @@ function viewDetails(row) {
|
||||
function reset() {
|
||||
form.value = {
|
||||
userId: undefined,
|
||||
deptId: undefined,
|
||||
|
||||
userName: undefined,
|
||||
userName: undefined,
|
||||
nickName: undefined,
|
||||
password: undefined,
|
||||
phonenumber: undefined,
|
||||
email: undefined,
|
||||
sex: undefined,
|
||||
status: "0",
|
||||
remark: undefined,
|
||||
postIds: [],
|
||||
roleId: "2" // 默认选择普通用户
|
||||
roleIds: [],
|
||||
avatar: undefined
|
||||
};
|
||||
proxy.resetForm("userRef");
|
||||
}
|
||||
@ -388,11 +423,6 @@ function reset() {
|
||||
function submitForm() {
|
||||
proxy.$refs["userRef"].validate(valid => {
|
||||
if (valid) {
|
||||
// 转换roleId为roleIds数组
|
||||
if (form.value.roleId) {
|
||||
form.value.roleIds = [form.value.roleId];
|
||||
}
|
||||
|
||||
if (form.value.userId != undefined) {
|
||||
updateUser(form.value).then(response => {
|
||||
proxy.$modal.msgSuccess("修改成功");
|
||||
@ -466,6 +496,37 @@ function submitFileForm() {
|
||||
proxy.$refs["uploadRef"].submit();
|
||||
}
|
||||
|
||||
/** 头像上传前处理 */
|
||||
function beforeAvatarUpload(file) {
|
||||
const isJPG = file.type === 'image/jpeg' || file.type === 'image/png';
|
||||
const isLt2M = file.size / 1024 / 1024 < 2;
|
||||
|
||||
if (!isJPG) {
|
||||
proxy.$modal.msgError('上传头像图片只能是JPG或PNG格式!');
|
||||
return false;
|
||||
}
|
||||
if (!isLt2M) {
|
||||
proxy.$modal.msgError('上传头像图片大小不能超过2MB!');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 头像上传成功处理 */
|
||||
function handleAvatarSuccess(res, file) {
|
||||
if (res.code === 200) {
|
||||
form.value.avatar = res.imgUrl;
|
||||
proxy.$modal.msgSuccess("上传成功");
|
||||
} else {
|
||||
proxy.$modal.msgError(res.msg || "上传失败");
|
||||
}
|
||||
}
|
||||
|
||||
/** 移除头像 */
|
||||
function handleRemoveAvatar() {
|
||||
form.value.avatar = undefined;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getList();
|
||||
getRoleOptions();
|
||||
@ -597,6 +658,13 @@ function getPostOptions() {
|
||||
:deep(.el-select__placeholder) {
|
||||
color: #96A0B5 !important;
|
||||
}
|
||||
.avatar-box{
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
object-fit: cover;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.custom-table {
|
||||
background-color: #292c38 !important;
|
||||
color: #ffffff;
|
||||
@ -920,4 +988,94 @@ function getPostOptions() {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -31,7 +31,8 @@ export default defineConfig(({ mode, command }) => {
|
||||
proxy: {
|
||||
// https://cn.vitejs.dev/config/#server-proxy
|
||||
'/dev-api': {
|
||||
target: 'http://10.0.0.17:8099',
|
||||
// target: 'http://10.0.0.17:8099',//昊天
|
||||
target: 'http://10.0.0.124:8080',//田哥
|
||||
changeOrigin: true,
|
||||
rewrite: (p) => p.replace(/^\/dev-api/, '')
|
||||
}
|
||||
|
||||