车辆监控

This commit is contained in:
任娜 2025-06-30 18:15:50 +08:00
parent 1bf47c141c
commit e1a82fb670
4 changed files with 1185 additions and 238 deletions

View File

@ -0,0 +1,576 @@
<template>
<div class="vehicle-cards">
<!-- 卡片网格布局 -->
<div class="cards-container">
<div class="cards-grid">
<div
v-for="vehicle in filteredVehicleData"
:key="vehicle.id"
class="vehicle-card"
:class="{
'task-status': vehicle.routeStatus === '任务中',
'idle-status': vehicle.routeStatus === '闲置中',
'waiting-status': vehicle.routeStatus === '等待中'
}"
>
<div class="card-content">
<!-- 第一行左侧图标+车辆编号和类型右侧状态 -->
<div class="card-row card-header">
<div class="left-content">
<img src="@/assets/images/carIcon.png" class="car-icon" alt="车辆图标" />
<div class="vehicle-info">
<span class="vehicle-id">{{ vehicle.carId }}</span>
<span class="vehicle-type">{{ vehicle.type }}</span>
</div>
</div>
<div class="right-content">
<span class="status-tag"
:class="{
'task': vehicle.routeStatus === '任务中',
'idle': vehicle.routeStatus === '闲置中',
'waiting': vehicle.routeStatus === '等待中'
}">
{{ vehicle.routeStatus }}
</span>
</div>
</div>
<!-- 第二行电池信息电压电流 -->
<div class="card-row battery-info">
<div class="info-item battery-status">
<div class="battery-container">
<div class="battery-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="14" viewBox="0 0 24 14">
<rect x="1" y="2" width="20" height="10" rx="1" ry="1" fill="transparent" stroke="#00ff84" stroke-width="1.5"/>
<rect x="21" y="4" width="2" height="6" rx="1" ry="1" fill="#00ff84"/>
<rect x="2" y="3" :width="getBatteryWidth(vehicle.soc)" height="8" rx="1" ry="1" fill="#00ff84"/>
</svg>
</div>
<span class="lightning-icon"></span>
<span class="percentage">{{ vehicle.soc }}</span>
</div>
</div>
<span class="divider">|</span>
<div class="info-item voltage">
<i class="el-icon voltage-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M14 9.5V6h2V2H8v4h2v3.5L2 20h20L14 9.5zm-6 8.5l5-5l5 5H8z"/></svg>
</i>
<span>{{ vehicle.voltage }}</span>
</div>
<span class="divider">|</span>
<div class="info-item current">
<i class="el-icon current-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M16 7l-4-4l-4 4l1.41 1.41L11 6.83V20h2V6.83l1.59 1.58L16 7z"/></svg>
</i>
<span>{{ vehicle.current }}</span>
</div>
</div>
<!-- 第三行位置信息 -->
<div class="card-row location-info">
<i class="el-icon location-icon">
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" data-v-78e17ca8=""><path fill="currentColor" d="M512 928c23.936 0 117.504-68.352 192.064-153.152C803.456 661.888 864 535.808 864 416c0-189.632-155.84-320-352-320S160 226.368 160 416c0 120.32 60.544 246.4 159.936 359.232C394.432 859.84 488 928 512 928zm0-435.2a64 64 0 1 0 0-128 64 64 0 0 0 0 128zm0 140.8a204.8 204.8 0 1 1 0-409.6 204.8 204.8 0 0 1 0 409.6z"></path></svg>
</i>
<span class="location-value">{{ vehicle.location }}</span>
</div>
<!-- 第四行负责人和电话 -->
<div class="card-row contact-info">
<div class="left-content">
<i class="el-icon person-icon">
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" data-v-78e17ca8=""><path fill="currentColor" d="M628.736 528.896A416 416 0 0 1 928 928H96a415.872 415.872 0 0 1 299.264-399.104L512 704l116.736-175.104zM720 304a208 208 0 1 1-416 0 208 208 0 0 1 416 0z"></path></svg>
</i>
<span class="contact-name">{{ vehicle.batteryLevel }}</span>
</div>
<div class="right-content">
<i class="el-icon phone-icon">
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" data-v-78e17ca8=""><path fill="currentColor" d="M768 896a128 128 0 0 0 128-128V256a128 128 0 0 0-128-128H256a128 128 0 0 0-128 128v512a128 128 0 0 0 128 128h512zm0 64H256a192 192 0 0 1-192-192V256a192 192 0 0 1 192-192h512a192 192 0 0 1 192 192v512a192 192 0 0 1-192 192z"></path><path fill="currentColor" d="M542.336 711.552a32 32 0 0 1-33.92-29.44 431.04 431.04 0 0 0-166.272-173.76 32 32 0 0 1-2.048-50.592l42.688-38.816a32 32 0 0 1 43.008 3.328 353.6 353.6 0 0 1 151.936 167.52 32 32 0 0 1-28.8 45.76l-.512-13.28c.448 4.48.704 8.96.512 13.28zm215.808-261.12a32 32 0 0 1-46.08-12.608 803.2 803.2 0 0 0-315.392-280.064 32.128 32.128 0 0 1-9.216-43.392l38.016-51.2a32.128 32.128 0 0 1 44.352-8.448c123.136 88.576 242.688 196.544 339.456 347.648a32 32 0 0 1-11.264 47.424l-39.872 1.344.064-.704z"></path></svg>
</i>
<span class="contact-phone">{{ vehicle.contactPhone }}</span>
</div>
</div>
<!-- 第五行品牌和公司 -->
<div class="card-row company-info">
<div class="left-content">
<i class="el-icon brand-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M12 4C7.59 4 4 7.59 4 12s3.59 8 8 8s8-3.59 8-8s-3.59-8-8-8zm0 14c-3.31 0-6-2.69-6-6s2.69-6 6-6s6 2.69 6 6s-2.69 6-6 6zm-1-10h2v4h-2zm0 5h2v2h-2z"/></svg>
</i>
<span class="brand-name">{{ vehicle.brand }}</span>
</div>
<div class="right-content">
<i class="el-icon company-icon">
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" data-v-78e17ca8=""><path fill="currentColor" d="M160 224a32 32 0 0 0-32 32v544a32 32 0 0 0 32 32h704a32 32 0 0 0 32-32V256a32 32 0 0 0-32-32H160zm0-64h704a96 96 0 0 1 96 96v544a96 96 0 0 1-96 96H160a96 96 0 0 1-96-96V256a96 96 0 0 1 96-96z"></path><path fill="currentColor" d="M464 224a32 32 0 0 0-32 32v128a32 32 0 0 0 32 32h96a32 32 0 0 0 32-32V256a32 32 0 0 0-32-32h-96zM96 384h128q32 0 32 32t-32 32H96q-32 0-32-32t32-32zm142.4 234.24L326.4 800h64.32l96-170.24-41.92-64.96L384 704l-88.96-149.76-57.6 64z"></path></svg>
</i>
<span class="company-name">{{ vehicle.organization }}</span>
</div>
</div>
<!-- 第六行最新更新时间 -->
<div class="card-row update-time">
<span class="update-label">最新更新时间</span>
<span class="update-value">{{ vehicle.lastTime }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- 底部统计和分页 -->
<div class="bottom-stats">
<div class="status-counters">
<div class="status-counter online">
<div class="counter-label">在线设备:</div>
<div class="counter-value">{{ onlineCount }}</div>
</div>
<div class="status-counter offline">
<div class="counter-label">离线设备:</div>
<span class="counter-value">{{ offlineCount }}</span>
</div>
<div class="status-counter fault">
<span class="counter-label">故障设备:</span>
<span class="counter-value">{{ faultCount }}</span>
</div>
</div>
<div class="pagination-wrapper">
<pagination
v-show="total > 0"
:total="total"
v-model:page="currentPage"
v-model:limit="pageSize"
@pagination="handlePagination"
/>
</div>
</div>
</div>
</template>
<script setup>
import { ref, defineProps, defineEmits, computed } from 'vue';
import Pagination from '@/components/Pagination/index.vue';
import car1 from '@/assets/images/car1.png';
import car2 from '@/assets/images/car2.png';
import car3 from '@/assets/images/car3.png';
import car4 from '@/assets/images/car4.png';
const props = defineProps({
vehicleData: {
type: Array,
required: true
},
filters: {
type: Object,
default: () => ({})
}
});
const emit = defineEmits(['view', 'control']);
// - 8
const currentPage = ref(1);
const pageSize = ref(8);
const total = computed(() => props.vehicleData.length);
//
const filteredVehicleData = computed(() => {
const start = (currentPage.value - 1) * pageSize.value;
const end = start + pageSize.value;
return props.vehicleData.slice(start, end);
});
//
const onlineCount = computed(() =>
props.vehicleData.filter(v => v.status === '在线').length
);
const offlineCount = computed(() =>
props.vehicleData.filter(v => v.status === '离线').length
);
const faultCount = computed(() =>
props.vehicleData.filter(v => v.status === '故障').length
);
//
function getCarImage(vehicle) {
switch (vehicle.status) {
case '在线':
return car1;
case '离线':
return car2;
case '故障':
return car3;
default:
return car4;
}
}
//
function getStatusType(status) {
switch (status) {
case '在线':
return 'success';
case '离线':
return 'info';
case '故障':
return 'danger';
default:
return '';
}
}
//
function getRouteStatusType(status) {
switch (status) {
case '任务中':
return 'success';
case '待命中':
case '闲置中':
return '';
case '充电中':
return 'warning';
case '等待中':
return 'info';
default:
return '';
}
}
//
function getBatteryPercentage(soc) {
return parseInt(soc) || 0;
}
//
function getBatteryWidth(soc) {
const percentage = getBatteryPercentage(soc);
// 1820
return Math.max(1, Math.min(18, (percentage / 100) * 18));
}
//
function getBatteryColor(soc) {
const percentage = getBatteryPercentage(soc);
if (percentage > 70) return '#67C23A';
if (percentage > 30) return '#E6A23C';
return '#F56C6C';
}
//
function handleView(row) {
emit('view', row);
}
//
function handleControl(row) {
emit('control', row);
}
//
function handlePagination({ page, limit }) {
currentPage.value = page;
pageSize.value = limit;
}
</script>
<style scoped lang="scss">
.vehicle-cards {
width: 100%;
display: flex;
flex-direction: column;
height: 100%;
}
.cards-container {
flex: 1;
overflow-y: auto;
max-height: calc(100% - 60px); /* 减去底部统计区域高度 */
padding: 10px 0;
}
.cards-grid {
width: 100%;
display: flex;
flex-wrap: wrap;
gap: 15px;
}
.vehicle-card {
background-color: #343744;
color: #ffffff;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
position: relative;
/* 顶部状态颜色 */
&.task-status {
border-top: 4px solid #5690e7;
}
&.idle-status {
border-top: 4px solid #c3c3c3;
}
&.waiting-status {
border-top: 4px solid #ff312f;
}
}
.card-content {
padding: 15px;
}
.card-row {
display: flex;
align-items: center;
margin-bottom: 12px;
&:last-child {
margin-bottom: 0;
}
&.card-header {
justify-content: space-between;
margin-bottom: 15px;
.left-content {
display: flex;
align-items: center;
gap: 8px;
.car-icon {
width: 24px;
height: 24px;
}
.vehicle-info {
display: flex;
flex-direction: column;
.vehicle-id {
font-size: 16px;
font-weight: bold;
}
.vehicle-type {
font-size: 12px;
color: #96a0b5;
}
}
}
.status-tag {
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
&.task {
background-color: #5690e7;
color: #fff;
}
&.idle {
background-color: #c3c3c3;
color: #fff;
}
&.waiting {
background-color: #ff312f;
color: #fff;
}
}
}
&.battery-info {
display: flex;
align-items: center;
gap: 8px;
.info-item {
display: flex;
align-items: center;
.battery-container {
display: flex;
align-items: center;
background-color: rgba(0, 255, 132, 0.2); /* #00ff84 with 20% opacity */
border-radius: 4px;
padding: 4px 8px;
.battery-icon {
display: flex;
align-items: center;
}
.lightning-icon {
font-size: 12px;
margin: 0 4px;
color: #00ff84;
}
.percentage {
font-size: 12px;
font-weight: bold;
color: #00ff84;
}
}
i.el-icon {
font-size: 18px;
color: #67C23A;
&.voltage-icon,
&.current-icon {
color: #409EFF;
}
}
}
.divider {
color: #5e6778;
}
}
&.location-info {
color: #96a0b5;
font-size: 14px;
i.location-icon {
color: #409EFF;
margin-right: 4px;
font-size: 18px;
}
}
&.contact-info,
&.company-info {
display: flex;
justify-content: space-between;
.left-content,
.right-content {
display: flex;
align-items: center;
gap: 4px;
color: #96a0b5;
font-size: 14px;
i {
color: #409EFF;
font-size: 18px;
}
}
}
&.update-time {
font-size: 12px;
color: #5e6778;
.update-label {
margin-right: 4px;
}
}
}
.bottom-stats {
display: flex;
align-items: center;
justify-content: space-between;
padding: 15px 5px;
border-top: 1px solid #4C4F5F;
.status-counters {
display: flex;
align-items: center;
gap: 10px;
.status-counter {
display: flex;
align-items: center;
.counter-label {
color: #96A0B5;
margin-right: 5px;
}
&.online .counter-value {
color: #67C23A;
}
&.offline .counter-value {
color: #909399;
}
&.fault .counter-value {
color: #F56C6C;
}
}
}
.pagination-wrapper {
margin-left: auto;
}
}
/* 自定义EL组件样式 */
:deep(.el-button.is-text) {
color: #347AE2;
}
:deep(.el-pagination) {
margin-top: 0;
justify-content: flex-end;
}
/* 自定义滚动条 */
.cards-container::-webkit-scrollbar {
width: 6px;
}
.cards-container::-webkit-scrollbar-thumb {
background-color: #4C4F5F;
border-radius: 6px;
}
.cards-container::-webkit-scrollbar-track {
background-color: #292c38;
}
/* 响应式设计 */
@media (max-width: 1600px) {
.vehicle-card {
width: calc(33.33% - 12px);
}
}
@media (max-width: 1200px) {
.vehicle-card {
width: calc(50% - 10px);
}
}
@media (max-width: 768px) {
.vehicle-card {
width: 100%;
}
.bottom-stats {
flex-direction: column;
gap: 15px;
align-items: flex-start;
.status-counters {
width: 100%;
justify-content: space-between;
}
.pagination-wrapper {
width: 100%;
display: flex;
justify-content: flex-end;
}
}
}
</style>

View File

@ -1,76 +1,98 @@
<template> <template>
<div class="search-filter"> <div class="search-filter">
<!-- 车牌号输入 --> <div class="search-area">
<el-input <el-input
v-model="searchText" v-model="searchText"
placeholder="请输入车牌号查询" placeholder="请输入车牌号查询"
clearable clearable
class="search-input" prefix-icon="Search"
@input="handleSearch" class="search-input"
prefix-icon="Search" @input="handleSearchInput"
/> @keyup.enter="handleSearch"
<!-- 车辆类型下拉 --> />
<el-select
v-model="vehicleType" <el-select v-model="vehicleType" placeholder="车辆类型" clearable class="search-select" @change="handleSearch">
placeholder="车辆类型" <el-option label="全部" value="" />
clearable <el-option label="驱动车" value="驱动车" />
class="search-select" <el-option label="巡检车" value="巡检车" />
@change="handleFilter" </el-select>
>
<el-option label="全部" value="" /> <el-select v-model="vehicleStatus" placeholder="车辆状态" clearable class="search-select" @change="handleSearch">
<el-option label="驱动车" value="驱动车" /> <el-option label="全部" value="" />
<el-option label="巡检车" value="巡检车" /> <el-option label="在线" value="在线" />
<!-- 可根据实际类型补充 --> <el-option label="离线" value="离线" />
</el-select> <el-option label="故障" value="故障" />
<!-- 车辆状态下拉 --> </el-select>
<el-select
v-model="vehicleStatus" <div class="search-buttons">
placeholder="车辆状态" <el-button type="primary" class="search-btn" @click="handleSearch">搜索</el-button>
clearable <el-button class="reset-btn" @click="handleReset">重置</el-button>
class="search-select" <el-button class="export-btn" @click="handleExport">导出</el-button>
@change="handleFilter" </div>
>
<el-option label="全部" value="" /> <div class="action-buttons">
<el-option label="在线" value="在线" /> <!-- 视图切换按钮 -->
<el-option label="离线" value="离线" /> <div class="view-switcher">
<el-option label="故障" value="故障" /> <div
</el-select> class="view-btn list-view"
:class="{'active': viewMode === 'list'}"
<div class="button-group"> @click="switchView('list')"
<!-- 搜索按钮 --> title="列表视图"
<el-button type="primary" class="search-btn" @click="handleFilter">搜索</el-button> >
<!-- 重置按钮 --> <svg-icon icon-class="table" />
<el-button class="reset-btn" @click="resetFilter">重置</el-button> </div>
</div> <div
class="view-btn card-view"
<div class="action-group"> :class="{'active': viewMode === 'card'}"
<!-- 导出按钮 --> @click="switchView('card')"
<el-button class="export-btn" @click="exportData">导出</el-button> title="卡片视图"
>
<svg-icon icon-class="dashboard" />
</div>
</div>
</div>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, defineEmits } from 'vue'; import { ref, defineProps, defineEmits, watchEffect } from 'vue';
import { Download, Search } from '@element-plus/icons-vue'; import { Search, Download } from '@element-plus/icons-vue';
const emit = defineEmits(['search', 'filter', 'reset', 'export']); const props = defineProps({
filters: {
type: Object,
default: () => ({})
},
viewMode: {
type: String,
default: 'list'
}
});
const emit = defineEmits(['filter', 'reset', 'export', 'switch-view']);
//
const searchText = ref(''); const searchText = ref('');
const vehicleType = ref(''); const vehicleType = ref('');
const vehicleStatus = ref(''); const vehicleStatus = ref('');
// // filters
function handleSearch() { watchEffect(() => {
emit('search', { if (props.filters) {
searchText: searchText.value, searchText.value = props.filters.searchText || '';
vehicleType: vehicleType.value, vehicleType.value = props.filters.vehicleType || '';
vehicleStatus: vehicleStatus.value vehicleStatus.value = props.filters.vehicleStatus || '';
}); }
});
//
function handleSearchInput() {
//
} }
// //
function handleFilter() { function handleSearch() {
emit('filter', { emit('filter', {
searchText: searchText.value, searchText: searchText.value,
vehicleType: vehicleType.value, vehicleType: vehicleType.value,
@ -78,107 +100,105 @@ function handleFilter() {
}); });
} }
// //
function resetFilter() { function handleReset() {
searchText.value = ''; searchText.value = '';
vehicleType.value = ''; vehicleType.value = '';
vehicleStatus.value = ''; vehicleStatus.value = '';
emit('reset'); emit('reset');
} }
// //
function exportData() { function handleExport() {
emit('export'); emit('export');
} }
//
function switchView(type) {
emit('switch-view', type);
}
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.search-filter { .search-filter {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 12px;
padding: 0;
background: transparent;
width: 100%; width: 100%;
margin-bottom: 20px;
} }
.search-input { .search-area {
width: 270px;
}
.search-select {
width: 140px;
}
.button-group {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px;
} .search-input {
width: 250px;
.action-group { margin-right: 10px;
margin-left: auto; }
}
.search-select {
@media (max-width: 1200px) { width: 150px;
.search-filter { margin-right: 10px;
flex-wrap: wrap; }
.search-buttons {
margin-right: 20px;
display: flex;
gap: 10px; gap: 10px;
} }
.search-input { .action-buttons {
width: 100%; margin-left: auto;
max-width: 270px;
}
.action-group {
margin-left: 0;
width: 100%;
display: flex; display: flex;
justify-content: flex-end; gap: 10px;
margin-top: 10px;
} .view-switcher {
} display: flex;
background-color: #343744;
@media (max-width: 768px) { border-radius: 8px;
.search-filter { overflow: hidden;
flex-direction: column;
align-items: flex-start; .view-btn {
} display: flex;
justify-content: center;
.search-input, .search-select { align-items: center;
width: 100%; width: 36px;
} height: 36px;
cursor: pointer;
.button-group, .action-group { transition: all 0.3s;
width: 100%;
justify-content: flex-start; &:hover {
background-color: #2B3B5A;
}
&.active {
background-color: #409eff;
svg {
color: white;
}
}
svg {
font-size: 18px;
color: #96A0B5;
}
}
}
} }
} }
//
:deep(.el-input__wrapper:hover) { :deep(.el-input__wrapper:hover) {
box-shadow: none !important; box-shadow: none !important;
} }
:deep(.el-select) { :deep(.el-select) {
border: none !important; border: none !important;
width: 140px !important; width: 150px !important;
// el-selectborder // el-selectborder
--el-border-color-hover: #343744 !important; --el-border-color-hover: #343744 !important;
--el-border-color: #343744 !important; --el-border-color: #343744 !important;
} }
:deep(.el-input__wrapper) {
background: #343744 !important;
border: none !important;
border-radius: 8px !important;
height: 36px !important;
font-size: 15px;
color: #96A0B5 !important;
box-shadow: none !important;
}
:deep(.el-select__wrapper.is-focused) { :deep(.el-select__wrapper.is-focused) {
box-shadow: none !important; box-shadow: none !important;
} }
@ -188,10 +208,17 @@ function exportData() {
color: #96A0B5 !important; color: #96A0B5 !important;
border-radius: 8px !important; border-radius: 8px !important;
height: 36px !important; height: 36px !important;
width: 140px;
font-size: 15px; font-size: 15px;
border: none !important; border: none !important;
}
:deep(.el-input__wrapper) {
background: #343744 !important;
color: #96A0B5 !important;
border-radius: 8px !important;
height: 36px !important;
border: none !important; border: none !important;
box-shadow: none !important;
} }
:deep(.el-select__placeholder) { :deep(.el-select__placeholder) {
@ -217,4 +244,50 @@ function exportData() {
background-color: #337ecc; background-color: #337ecc;
} }
} }
//
@media (max-width: 1200px) {
.search-area {
flex-wrap: wrap;
gap: 10px;
.search-input {
width: 100%;
max-width: 250px;
margin-right: 0;
}
.search-select {
margin-right: 0;
}
.search-buttons {
margin-right: 0;
}
.action-buttons {
margin-left: 0;
margin-top: 10px;
}
}
}
@media (max-width: 768px) {
.search-area {
flex-direction: column;
align-items: flex-start;
.search-input,
.search-select {
width: 100%;
max-width: none;
}
.search-buttons,
.action-buttons {
width: 100%;
margin-top: 10px;
}
}
}
</style> </style>

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="vehicle-table"> <div class="vehicle-table">
<!-- 搜索区域 --> <!-- 搜索区域 -->
<div class="search-area"> <div class="search-area" v-if="!hideSearch">
<el-input <el-input
v-model="searchText" v-model="searchText"
placeholder="请输入车牌号查询" placeholder="请输入车牌号查询"
@ -32,83 +32,103 @@
</div> </div>
<div class="action-buttons"> <div class="action-buttons">
<!-- 两个Tab切换的图标1.png 2.png --> <!-- 视图切换按钮 -->
<!-- 默认1.png 显示当前表格点击2.png 显示卡片样式 --> <div class="view-switcher">
<div
class="view-btn list-view"
:class="{'active': !isCardView}"
@click="switchView('list')"
title="列表视图"
>
<svg-icon icon-class="table" />
</div>
<div
class="view-btn card-view"
:class="{'active': isCardView}"
@click="switchView('card')"
title="卡片视图"
>
<svg-icon icon-class="dashboard" />
</div>
</div>
</div> </div>
</div> </div>
<!-- 表格 --> <!-- 表格区域 -->
<el-table <div class="table-container" :class="{ 'full-height': hideSearch }">
:data="filteredVehicleData" <!-- 表格 -->
style="width: 100%" <el-table
@selection-change="handleSelectionChange" :data="filteredVehicleData"
:header-cell-style="{ backgroundColor: '#343744', color: '#fff' }" style="width: 100%"
class="custom-table" @selection-change="handleSelectionChange"
:bg-color="'#292C38'" :header-cell-style="{ backgroundColor: '#343744', color: '#fff' }"
> class="custom-table"
<el-table-column type="selection" width="55" /> :bg-color="'#292C38'"
>
<el-table-column label="序号" width="70"> <el-table-column type="selection" width="55" />
<template #default="scope">
{{ scope.$index + 1 }} <el-table-column label="序号" width="70">
</template> <template #default="scope">
</el-table-column> {{ scope.$index + 1 }}
</template>
<el-table-column label="操作" width="120"> </el-table-column>
<template #default="scope">
<el-button <el-table-column label="操作" width="120">
link <template #default="scope">
text <el-button
type="primary" link
text
@click="handleView(scope.row)" type="primary"
> @click="handleView(scope.row)"
查看 >
</el-button> 查看
</template> </el-button>
</el-table-column> </template>
</el-table-column>
<el-table-column prop="carId" label="车辆号" />
<el-table-column prop="type" label="类型" /> <el-table-column prop="carId" label="车辆号" />
<el-table-column prop="brand" label="品牌" /> <el-table-column prop="type" label="类型" />
<el-table-column prop="organization" label="所属单位" width="120"/> <el-table-column prop="brand" label="品牌" />
<el-table-column prop="number" label="当前故障数" width="100" /> <el-table-column prop="organization" label="所属单位" width="120"/>
<el-table-column prop="number" label="当前故障数" width="100" />
<el-table-column prop="routeStatus" label="作业状态">
<template #default="scope"> <el-table-column prop="routeStatus" label="作业状态">
<el-tag :type="getRouteStatusType(scope.row.routeStatus)"> <template #default="scope">
{{ scope.row.routeStatus }} <el-tag :type="getRouteStatusType(scope.row.routeStatus)">
</el-tag> {{ scope.row.routeStatus }}
</template> </el-tag>
</el-table-column> </template>
</el-table-column>
<el-table-column prop="status" label="车辆状态">
<template #default="scope"> <el-table-column prop="status" label="车辆状态">
<el-tag :type="scope.row.status === '在线' ? 'success' : 'danger'"> <template #default="scope">
{{ scope.row.status }} <el-tag :type="scope.row.status === '在线' ? 'success' : 'danger'">
</el-tag> {{ scope.row.status }}
</template> </el-tag>
</el-table-column> </template>
</el-table-column>
<el-table-column prop="chargeStatus" label="充电状态"/>
<el-table-column prop="voltage" label="总电压"/> <el-table-column prop="chargeStatus" label="充电状态"/>
<el-table-column prop="current" label="总电流" /> <el-table-column prop="voltage" label="总电压"/>
<el-table-column prop="soc" label="SOC" /> <el-table-column prop="current" label="总电流" />
<el-table-column prop="lastTime" label="最新时间" width="170" /> <el-table-column prop="soc" label="SOC" />
<el-table-column prop="location" label="最新位置" /> <el-table-column prop="lastTime" label="最新时间" width="170" />
<el-table-column prop="batteryLevel" label="负责人" /> <el-table-column prop="location" label="最新位置" />
<el-table-column prop="contactPhone" label="负责人电话" width="130" /> <el-table-column prop="batteryLevel" label="负责人" />
</el-table> <el-table-column prop="contactPhone" label="负责人电话" width="130" />
</el-table>
</div>
<!-- 分页 --> <!-- 分页 -->
<pagination <div class="pagination-container">
v-show="total > 0" <pagination
:total="total" v-show="total > 0"
v-model:page="currentPage" :total="total"
v-model:limit="pageSize" v-model:page="currentPage"
@pagination="handlePagination" v-model:limit="pageSize"
/> @pagination="handlePagination"
/>
</div>
</div> </div>
</template> </template>
@ -125,10 +145,14 @@ const props = defineProps({
filters: { filters: {
type: Object, type: Object,
default: () => ({}) default: () => ({})
},
hideSearch: {
type: Boolean,
default: false
} }
}); });
const emit = defineEmits(['view', 'control', 'selection-change', 'search', 'filter', 'reset', 'export']); const emit = defineEmits(['view', 'control', 'selection-change', 'search', 'filter', 'reset', 'export', 'switch-view']);
// //
const searchText = ref(''); const searchText = ref('');
@ -143,6 +167,9 @@ const total = computed(() => props.vehicleData.length);
// //
const selectedRows = ref([]); const selectedRows = ref([]);
//
const isCardView = ref(false);
// filters // filters
watchEffect(() => { watchEffect(() => {
if (props.filters) { if (props.filters) {
@ -233,6 +260,12 @@ function handleControl(row) {
emit('control', row); emit('control', row);
} }
//
function switchView(viewType) {
isCardView.value = viewType === 'card';
emit('switch-view', viewType);
}
// //
function handleSelectionChange(selection) { function handleSelectionChange(selection) {
selectedRows.value = selection; selectedRows.value = selection;
@ -249,8 +282,10 @@ function handlePagination({ page, limit }) {
<style scoped lang="scss"> <style scoped lang="scss">
.vehicle-table { .vehicle-table {
width: 100%; width: 100%;
background-color: #292c38; background-color: transparent;
border-radius: 4px; display: flex;
flex-direction: column;
height: 100%;
} }
.search-area { .search-area {
@ -278,9 +313,58 @@ function handlePagination({ page, limit }) {
margin-left: auto; margin-left: auto;
display: flex; display: flex;
gap: 10px; gap: 10px;
.view-switcher {
display: flex;
background-color: #343744;
border-radius: 8px;
overflow: hidden;
.view-btn {
display: flex;
justify-content: center;
align-items: center;
width: 36px;
height: 36px;
cursor: pointer;
transition: all 0.3s;
&:hover {
background-color: #2B3B5A;
}
&.active {
background-color: #409eff;
svg {
color: white;
}
}
svg {
font-size: 18px;
color: #96A0B5;
}
}
}
} }
} }
.table-container {
flex: 1;
overflow: auto;
&.full-height {
height: calc(100% - 50px); /* 留出分页高度 */
}
}
.pagination-container {
display: flex;
justify-content: flex-end;
padding: 15px 0;
}
// //
:deep(.el-input__wrapper:hover) { :deep(.el-input__wrapper:hover) {
box-shadow: none !important; box-shadow: none !important;
@ -342,7 +426,7 @@ function handlePagination({ page, limit }) {
/* 自定义表格样式 */ /* 自定义表格样式 */
.custom-table { .custom-table {
background-color: #292c38 !important; background-color: transparent !important;
color: #ffffff; color: #ffffff;
margin-bottom: 20px; margin-bottom: 20px;
border-radius: 4px; border-radius: 4px;
@ -363,11 +447,11 @@ function handlePagination({ page, limit }) {
/* 确保整个表格区域背景色 */ /* 确保整个表格区域背景色 */
:deep(.el-table) { :deep(.el-table) {
background-color: #292c38 !important; background-color: transparent !important;
} }
:deep(.el-table__body) { :deep(.el-table__body) {
background-color: #292c38 !important; background-color: transparent !important;
} }
:deep(.el-table__inner-wrapper::before) { :deep(.el-table__inner-wrapper::before) {
@ -418,7 +502,7 @@ function handlePagination({ page, limit }) {
td { td {
height: 68px!important; height: 68px!important;
background-color: #292c38; background-color: transparent;
color: #ffffff; color: #ffffff;
border-bottom: 1px solid #4C4F5F; border-bottom: 1px solid #4C4F5F;
} }
@ -465,8 +549,8 @@ function handlePagination({ page, limit }) {
/* 全局表格样式覆盖,防止闪白 */ /* 全局表格样式覆盖,防止闪白 */
:deep(.el-table) { :deep(.el-table) {
--el-table-bg-color: #292c38 !important; --el-table-bg-color: transparent !important;
--el-table-tr-bg-color: #292c38 !important; --el-table-tr-bg-color: transparent !important;
--el-table-border-color: #4C4F5F !important; --el-table-border-color: #4C4F5F !important;
--el-table-header-bg-color: #343744 !important; --el-table-header-bg-color: #343744 !important;
--el-table-row-hover-bg-color: #2B3B5A !important; --el-table-row-hover-bg-color: #2B3B5A !important;
@ -476,6 +560,20 @@ function handlePagination({ page, limit }) {
} }
} }
/* 自定义滚动条 */
.table-container::-webkit-scrollbar {
width: 6px;
}
.table-container::-webkit-scrollbar-thumb {
background-color: #4C4F5F;
border-radius: 6px;
}
.table-container::-webkit-scrollbar-track {
background-color: #292c38;
}
// //
@media (max-width: 1200px) { @media (max-width: 1200px) {
.search-area { .search-area {

View File

@ -12,18 +12,83 @@
class="stats-section" class="stats-section"
/> />
<!-- 表格区域 --> <!-- 内容容器 - 包含搜索区域和内容区域 -->
<div class="app-container"> <div class="app-container">
<VehicleTable <!-- 搜索和过滤区域 -->
:vehicleData="filteredVehicleData" <div class="search-area">
:filters="filters" <el-input
@view="viewVehicleDetail" v-model="searchText"
@control="controlVehicle" placeholder="请输入车牌号查询"
@selection-change="handleSelectionChange" clearable
@filter="handleFilter" prefix-icon="Search"
@reset="resetFilter" class="search-input"
@export="exportVehicleData" @keyup.enter="handleSearch"
/> />
<el-select v-model="vehicleType" placeholder="车辆类型" clearable class="search-select" @change="handleSearch">
<el-option label="全部" value="" />
<el-option label="驱动车" value="驱动车" />
<el-option label="巡检车" value="巡检车" />
</el-select>
<el-select v-model="vehicleStatus" placeholder="车辆状态" clearable class="search-select" @change="handleSearch">
<el-option label="全部" value="" />
<el-option label="在线" value="在线" />
<el-option label="离线" value="离线" />
<el-option label="故障" value="故障" />
</el-select>
<div class="search-buttons">
<el-button type="primary" class="search-btn" @click="handleSearch">搜索</el-button>
<el-button class="reset-btn" @click="resetFilter">重置</el-button>
<el-button class="export-btn" @click="exportVehicleData">导出</el-button>
</div>
<div class="action-buttons">
<!-- 视图切换按钮 -->
<div class="view-switcher">
<div
class="view-btn list-view"
:class="{'active': viewMode === 'list'}"
@click="handleViewSwitch('list')"
title="列表视图"
>
<svg-icon icon-class="table" />
</div>
<div
class="view-btn card-view"
:class="{'active': viewMode === 'card'}"
@click="handleViewSwitch('card')"
title="卡片视图"
>
<svg-icon icon-class="dashboard" />
</div>
</div>
</div>
</div>
<!-- 内容区域 -->
<div class="content-area">
<!-- 列表视图 -->
<VehicleTable
v-if="viewMode === 'list'"
:vehicleData="filteredVehicleData"
:filters="filters"
@view="viewVehicleDetail"
@control="controlVehicle"
@selection-change="handleSelectionChange"
:hide-search="true"
/>
<!-- 卡片视图 -->
<VehicleCards
v-else
:vehicleData="filteredVehicleData"
:filters="filters"
@view="viewVehicleDetail"
@control="controlVehicle"
/>
</div>
</div> </div>
</div> </div>
@ -39,11 +104,12 @@
</template> </template>
<script setup> <script setup>
import { ref, defineEmits } from 'vue'; import { ref, defineEmits, computed } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus'; import { ElMessage, ElMessageBox } from 'element-plus';
import { Close } from '@element-plus/icons-vue'; import { Close, Search } from '@element-plus/icons-vue';
import VehicleStats from '../../../components/car/VehicleStats.vue'; import VehicleStats from '../../../components/car/VehicleStats.vue';
import VehicleTable from '../../../components/car/VehicleTable.vue'; import VehicleTable from '../../../components/car/VehicleTable.vue';
import VehicleCards from '../../../components/car/VehicleCards.vue';
import VehicleDetail from './detail.vue'; import VehicleDetail from './detail.vue';
import { useVehicle } from '../../../composables/useVehicle'; import { useVehicle } from '../../../composables/useVehicle';
@ -60,11 +126,16 @@ const {
exportData exportData
} = useVehicle(); } = useVehicle();
//
const searchText = ref('');
const vehicleType = ref('');
const vehicleStatus = ref('');
// //
const activeStatIndex = ref(0); const activeStatIndex = ref(0);
// // -
const showMap = ref(false); const viewMode = ref('list');
// //
const selectedRows = ref([]); const selectedRows = ref([]);
@ -73,11 +144,6 @@ const selectedRows = ref([]);
const showDetail = ref(false); const showDetail = ref(false);
const currentVehicle = ref(null); const currentVehicle = ref(null);
//
function toggleMap() {
showMap.value = !showMap.value;
}
// - // -
function viewVehicleDetail(vehicle) { function viewVehicleDetail(vehicle) {
currentVehicle.value = vehicle; currentVehicle.value = vehicle;
@ -89,6 +155,11 @@ function closeDetail() {
showDetail.value = false; showDetail.value = false;
} }
//
function handleViewSwitch(type) {
viewMode.value = type;
}
// //
function controlVehicle(vehicle) { function controlVehicle(vehicle) {
ElMessageBox.confirm( ElMessageBox.confirm(
@ -125,21 +196,20 @@ function handleSelectionChange(rows) {
} }
// //
function handleSearch(keyword) { function handleSearch() {
// //
ElMessage({ updateFilters({
message: `搜索关键词: ${keyword}`, searchText: searchText.value,
type: 'success' vehicleType: vehicleType.value,
vehicleStatus: vehicleStatus.value
}); });
} }
//
function handleFilter(filterValues) {
updateFilters(filterValues);
}
// //
function resetFilter() { function resetFilter() {
searchText.value = '';
vehicleType.value = '';
vehicleStatus.value = '';
resetFilters(); resetFilters();
} }
@ -168,7 +238,17 @@ function exportVehicleData() {
border-radius: 10px; border-radius: 10px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: auto; overflow: hidden;
min-height: calc(100vh - 250px);
}
.content-area {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
height: calc(100% - 70px); /* 减去搜索区域的高度 */
min-height: 500px;
} }
.detail-view { .detail-view {
@ -177,6 +257,109 @@ function exportVehicleData() {
animation: fadeIn 0.3s ease; animation: fadeIn 0.3s ease;
} }
/* 搜索区域样式 */
.search-area {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
padding-bottom: 15px;
margin-bottom: 15px;
border-bottom: 1px solid #343744;
.search-input {
width: 250px;
}
.search-select {
width: 150px;
}
.search-buttons {
display: flex;
gap: 10px;
}
.action-buttons {
margin-left: auto;
display: flex;
gap: 10px;
.view-switcher {
display: flex;
background-color: #343744;
border-radius: 8px;
overflow: hidden;
.view-btn {
display: flex;
justify-content: center;
align-items: center;
width: 36px;
height: 36px;
cursor: pointer;
transition: all 0.3s;
&:hover {
background-color: #2B3B5A;
}
&.active {
background-color: #409eff;
:deep(svg) {
color: white;
}
}
:deep(svg) {
font-size: 18px;
color: #96A0B5;
}
}
}
}
}
/* 自定义输入框样式 */
:deep(.el-input__wrapper) {
background: #343744 !important;
color: #96A0B5 !important;
border-radius: 8px !important;
height: 36px !important;
border: none !important;
box-shadow: none !important;
}
:deep(.el-select__wrapper) {
background: #343744 !important;
color: #96A0B5 !important;
border-radius: 8px !important;
height: 36px !important;
font-size: 15px;
border: none !important;
}
:deep(.el-button) {
background: #343744;
border-color: transparent;
border-radius: 8px;
height: 36px;
&.el-button--primary {
background-color: #409eff;
}
&:hover, &:focus {
background: #2B3B5A;
border-color: transparent;
}
&.el-button--primary:hover, &.el-button--primary:focus {
background-color: #337ecc;
}
}
/* 淡入淡出过渡效果 */ /* 淡入淡出过渡效果 */
.fade-enter-active, .fade-enter-active,
.fade-leave-active { .fade-leave-active {
@ -202,5 +385,22 @@ function exportVehicleData() {
.detail-view { .detail-view {
min-height: calc(100vh - 80px); min-height: calc(100vh - 80px);
} }
.search-area {
flex-direction: column;
align-items: flex-start;
.search-input,
.search-select {
width: 100%;
}
.action-buttons {
margin-left: 0;
margin-top: 10px;
width: 100%;
justify-content: flex-end;
}
}
} }
</style> </style>