This commit is contained in:
任娜 2025-07-09 15:47:27 +08:00
commit 23c1e1796f
75 changed files with 7615 additions and 1461 deletions

View File

@ -111,3 +111,17 @@ getStyle(type, item, status) {
当前项目用于青岛机场正式版
/** 获取用户列表 */
function getUserOptions() {
getRoleUsers(3).then(res => {
userOptions.value = res.rows;
});
}
使用SockJS + STOMP协议
滑出蓝色 滑入黄色

View File

@ -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"

View 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'
})
}

View File

@ -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) {

View 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'
})
}

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 B

BIN
src/assets/images/2btn.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 B

BIN
src/assets/images/6btn.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 339 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 609 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 612 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 597 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 658 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 693 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 951 B

BIN
src/assets/images/del.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 491 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 604 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 454 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 389 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
src/assets/images/tab1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 B

BIN
src/assets/images/tab2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 B

BIN
src/assets/images/tab3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 446 B

BIN
src/assets/images/tab4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 363 B

BIN
src/assets/images/tab5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 321 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 571 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
src/assets/images/znzBg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -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;

View File

@ -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

View File

@ -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;

View File

@ -102,6 +102,7 @@ const statistics = computed(() => {
<style lang="scss" scoped>
.stats-container {
width: 100%;
}
.stat-cards {

View File

@ -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;

View File

@ -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>

View File

@ -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">&gt;</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>

View File

@ -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>

View 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>

View 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>

View File

@ -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>

File diff suppressed because it is too large Load Diff

View 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>

View File

@ -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>

View File

@ -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);
// propsvehicleMovementControl
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>

View 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';
// SockJSpolyfill
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>

View 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';
// SockJSpolyfill
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>

View 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>

View 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>

View 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>

View File

@ -76,7 +76,7 @@ onMounted(() => {
.car-alarm-container {
position: absolute;
left: 20px;
top: 50px;
top: 5%;
display: flex;
align-items: center;
gap:24px;

View File

@ -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;
}

View File

@ -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);
}
}
});
}

View 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
View 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;

View File

@ -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;

View File

@ -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;
}
}
}

View File

@ -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;

View File

@ -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) {
// editableTabsValuegetFirstTypeList
}
})
.catch(() => {
//
});
}
});
};
const handleTabClick = (tab) => {
editableTabsValue.value = tab.paneName;
queryParams.firstType = tab.paneName;
// getListwatch
// 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);
// firstTypeListtypeId
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;

View File

@ -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>

View File

@ -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;

View File

@ -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) {
// roleIdroleIds
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>

View File

@ -215,7 +215,7 @@ function getList() {
function handleQuery() {
queryParams.value.pageNum = 1;
getList();
}
}
/** 重置按钮操作 */
function resetQuery() {
dateRange.value = [];

View File

@ -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) {
// roleIdroleIds
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>

View File

@ -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/, '')
}