实现页面类型管理

This commit is contained in:
renna 2025-06-30 16:53:06 +08:00
parent 6402fbb3ef
commit 1bf47c141c
32 changed files with 6916 additions and 678 deletions

View File

@ -110,7 +110,4 @@ getStyle(type, item, status) {
实现功能计划: 当前项目用于青岛机场正式版
1.我目前需要脱离后台进行页面展示,给增加完善的模拟数据;
2.找一个线上地址测试地图的平台概览页面显示
3.添加新图层、绘制路线,引入绘制线、绘制面、绘制点

93
public/dianziweilan.json Normal file
View File

@ -0,0 +1,93 @@
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
120.07177603096052,
36.370812533360755
],
[
120.08385709134191,
36.33999243140878
],
[
120.08565563667813,
36.34060862711547
],
[
120.08669982868061,
36.34062878164365
],
[
120.07455220429061,
36.37193239994289
],
[
120.07234631337181,
36.37156118999518
],
[
120.07177603096052,
36.370812533360755
]
]
]
},
"properties": {
"type": "polygon",
"area": 671992.2620217785,
"perimeter": 6459.222679044949,
"vertices": 6
}
},
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
120.09357854630166,
36.38111581893121
],
[
120.09150927608138,
36.38118099552585
],
[
120.09017484057917,
36.380485019173435
],
[
120.10265440053722,
36.34926707805199
],
[
120.10434101414667,
36.34985392006327
],
[
120.1053224475362,
36.35065874503395
],
[
120.09357854630166,
36.38111581893121
]
]
]
},
"properties": {
"type": "polygon",
"area": 731220.4002154289,
"perimeter": 6460.346045623342,
"vertices": 6
}
}
]
}

4044
public/roadTest.json Normal file

File diff suppressed because it is too large Load Diff

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 486 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 509 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 435 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 881 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -209,7 +209,7 @@ function handleCurrentChange(val) {
// width: 100%; // width: 100%;
// Element Plus // Element Plus
// ::v-deep(.el-pagination) { // :deep(.el-pagination) {
// padding: 0; // padding: 0;
// background: none; // background: none;
// border: 1px solid #0a4273; // border: 1px solid #0a4273;

View File

@ -157,11 +157,11 @@ function exportData() {
} }
} }
::v-deep(.el-input__wrapper:hover) { :deep(.el-input__wrapper:hover) {
box-shadow: none !important; box-shadow: none !important;
} }
::v-deep(.el-select) { :deep(.el-select) {
border: none !important; border: none !important;
width: 140px !important; width: 140px !important;
// el-selectborder // el-selectborder
@ -169,7 +169,7 @@ function exportData() {
--el-border-color: #343744 !important; --el-border-color: #343744 !important;
} }
::v-deep(.el-input__wrapper) { :deep(.el-input__wrapper) {
background: #343744 !important; background: #343744 !important;
border: none !important; border: none !important;
border-radius: 8px !important; border-radius: 8px !important;
@ -179,11 +179,11 @@ function exportData() {
box-shadow: none !important; box-shadow: none !important;
} }
::v-deep(.el-select__wrapper.is-focused) { :deep(.el-select__wrapper.is-focused) {
box-shadow: none !important; box-shadow: none !important;
} }
::v-deep(.el-select__wrapper) { :deep(.el-select__wrapper) {
background: #343744 !important; background: #343744 !important;
color: #96A0B5 !important; color: #96A0B5 !important;
border-radius: 8px !important; border-radius: 8px !important;
@ -194,11 +194,11 @@ function exportData() {
border: none !important; border: none !important;
} }
::v-deep(.el-select__placeholder) { :deep(.el-select__placeholder) {
color: #96A0B5 !important; color: #96A0B5 !important;
} }
::v-deep(.el-button) { :deep(.el-button) {
background: #343744; background: #343744;
border-color: transparent; border-color: transparent;
border-radius: 8px; border-radius: 8px;

View File

@ -28,10 +28,13 @@
<div class="search-buttons"> <div class="search-buttons">
<el-button type="primary" class="search-btn" @click="handleSearch">搜索</el-button> <el-button type="primary" class="search-btn" @click="handleSearch">搜索</el-button>
<el-button class="reset-btn" @click="handleReset">重置</el-button> <el-button class="reset-btn" @click="handleReset">重置</el-button>
<el-button class="export-btn" @click="handleExport">导出</el-button>
</div> </div>
<div class="action-buttons"> <div class="action-buttons">
<el-button class="export-btn" @click="handleExport">导出</el-button> <!-- 两个Tab切换的图标1.png 2.png -->
<!-- 默认1.png 显示当前表格点击2.png 显示卡片样式 -->
</div> </div>
</div> </div>

View File

@ -135,11 +135,11 @@ function handlePagination({ page: newPage, limit: newLimit }) {
} }
// //
::v-deep(.el-input__wrapper:hover) { :deep(.el-input__wrapper:hover) {
box-shadow: none !important; box-shadow: none !important;
} }
::v-deep(.el-select) { :deep(.el-select) {
border: none !important; border: none !important;
width: 160px !important; width: 160px !important;
// el-selectborder // el-selectborder
@ -147,11 +147,11 @@ function handlePagination({ page: newPage, limit: newLimit }) {
--el-border-color: #343744 !important; --el-border-color: #343744 !important;
} }
::v-deep(.el-select__wrapper.is-focused) { :deep(.el-select__wrapper.is-focused) {
box-shadow: none !important; box-shadow: none !important;
} }
::v-deep(.el-select__wrapper) { :deep(.el-select__wrapper) {
background: #343744 !important; background: #343744 !important;
color: #96A0B5 !important; color: #96A0B5 !important;
border-radius: 8px !important; border-radius: 8px !important;
@ -161,7 +161,7 @@ function handlePagination({ page: newPage, limit: newLimit }) {
border: none !important; border: none !important;
} }
::v-deep(.el-input__wrapper) { :deep(.el-input__wrapper) {
background: #343744 !important; background: #343744 !important;
color: #96A0B5 !important; color: #96A0B5 !important;
border-radius: 8px !important; border-radius: 8px !important;
@ -170,7 +170,7 @@ function handlePagination({ page: newPage, limit: newLimit }) {
box-shadow: none !important; box-shadow: none !important;
} }
::v-deep(.el-select__placeholder) { :deep(.el-select__placeholder) {
color: #96A0B5 !important; color: #96A0B5 !important;
} }

View File

@ -8,8 +8,6 @@
<OpenLayersZoomControl <OpenLayersZoomControl
:map="map" :map="map"
:resetView="resetView" :resetView="resetView"
:routeLayerVisible="routeLayerVisible"
@layerToggle="toggleRouteLayer"
@compass="compass" @compass="compass"
@zoomIn="zoomIn" @zoomIn="zoomIn"
@zoomOut="zoomOut" @zoomOut="zoomOut"
@ -25,13 +23,16 @@
<!-- 比例尺控件传递map对象 --> <!-- 比例尺控件传递map对象 -->
<OpenLayersScaleControl :map="map" /> <OpenLayersScaleControl :map="map" />
<!-- 路线绘制控件 -->
<RouteDrawControl :map="map" ref="routeDrawControlRef" v-if="map" />
<!-- 插槽允许父组件添加其他控件 --> <!-- 插槽允许父组件添加其他控件 -->
<slot></slot> <slot></slot>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, onMounted, onUnmounted, watch } from "vue"; import { ref, onMounted, onUnmounted } from "vue";
import Map from "ol/Map"; import Map from "ol/Map";
import View from "ol/View"; import View from "ol/View";
import TileLayer from "ol/layer/Tile"; import TileLayer from "ol/layer/Tile";
@ -47,18 +48,7 @@ import OpenLayersMapInfo from "./controls/OpenLayersMapInfo.vue";
import OverviewMap from 'ol/control/OverviewMap'; import OverviewMap from 'ol/control/OverviewMap';
import ScaleLine from 'ol/control/ScaleLine'; import ScaleLine from 'ol/control/ScaleLine';
import OpenLayersScaleControl from "./controls/OpenLayersScaleControl.vue"; import OpenLayersScaleControl from "./controls/OpenLayersScaleControl.vue";
import OSM from 'ol/source/OSM'; import RouteDrawControl from "./controls/RouteDrawControl.vue";
import VectorLayer from 'ol/layer/Vector';
import VectorSource from 'ol/source/Vector';
import Feature from 'ol/Feature';
import Point from 'ol/geom/Point';
import LineString from 'ol/geom/LineString';
import Polygon from 'ol/geom/Polygon';
import Style from 'ol/style/Style';
import Icon from 'ol/style/Icon';
import Stroke from 'ol/style/Stroke';
import Fill from 'ol/style/Fill';
import { fromLonLat } from 'ol/proj';
// EPSG:4528 // EPSG:4528
proj4.defs( proj4.defs(
@ -92,12 +82,6 @@ const minZoom = 7; // 最小缩放级别
const maxZoom = 15; // const maxZoom = 15; //
const initialZoom = 8; // 7.578 const initialZoom = 8; // 7.578
const routeLayerVisible = ref(false); // 线
function toggleRouteLayer() {
routeLayerVisible.value = !routeLayerVisible.value;
}
onMounted(() => { onMounted(() => {
// //
let savedView = null; let savedView = null;
@ -130,19 +114,6 @@ onMounted(() => {
rotation: savedView ? savedView.rotation : rotation, // 使 rotation: savedView ? savedView.rotation : rotation, // 使
}); });
// 使OSM
const baseLayer = new TileLayer({
source: new OSM()
});
//
const vectorSource = new VectorSource();
const vectorLayer = new VectorLayer({
source: vectorSource,
zIndex: 2,
visible: routeLayerVisible.value
});
// .ol3 // .ol3
map.value = new Map({ map.value = new Map({
target: mapContainer.value, target: mapContainer.value,
@ -157,7 +128,18 @@ onMounted(() => {
constrainResolution: true, constrainResolution: true,
}), }),
]), ]),
layers: [baseLayer, vectorLayer], layers: [
// 使TileLayerTileSuperMapRest
new TileLayer({
source: new TileSuperMapRest({
url: "http://10.96.3.10:8090/iserver/services/map-QDJC_DT-GX3/rest/maps/QDJC2DZT",
projection: "EPSG:4528",
tileGrid: tileGrid,
wrapX: false,
attributions: "", //
}),
}),
],
view: view, view: view,
}); });
@ -206,73 +188,7 @@ onMounted(() => {
} }
}, 500); }, 500);
//
const pointFeature = new Feature({
geometry: new Point([13394416, 4136655])
});
pointFeature.setStyle(new Style({
image: new Icon({
src: 'https://openlayers.org/en/latest/examples/data/icon.png',
scale: 0.7
})
}));
vectorSource.addFeature(pointFeature);
// 线
const lineFeature = new Feature({
geometry: new LineString([
[13394416, 4136655],
[13395000, 4137000]
])
});
lineFeature.setStyle(new Style({
stroke: new Stroke({
color: 'blue',
width: 2
})
}));
vectorSource.addFeature(lineFeature);
//
const polygonFeature = new Feature({
geometry: new Polygon([[
[13394416, 4136655],
[13395000, 4136655],
[13395000, 4137000],
[13394416, 4137000],
[13394416, 4136655]
]])
});
polygonFeature.setStyle(new Style({
stroke: new Stroke({ color: 'red', width: 2 }),
fill: new Fill({ color: 'rgba(255,0,0,0.2)' })
}));
vectorSource.addFeature(polygonFeature);
// --- 线 ---
function randomLngLat() {
const lng = 120.1 + Math.random() * 0.4;
const lat = 35.9 + Math.random() * 0.4;
return [lng, lat];
}
// 3线5~8
for (let i = 0; i < 3; i++) {
const points = [];
const count = 5 + Math.floor(Math.random() * 4);
for (let j = 0; j < count; j++) {
points.push(fromLonLat(randomLngLat()));
}
const lineFeature = new Feature({
geometry: new LineString(points)
});
lineFeature.setStyle(new Style({
stroke: new Stroke({
color: ['red', 'blue', 'green'][i % 3],
width: 3
})
}));
vectorSource.addFeature(lineFeature);
}
// //
setTimeout(() => { setTimeout(() => {
@ -299,10 +215,6 @@ onMounted(() => {
// OpenLayers // OpenLayers
// UI // UI
}); });
watch(routeLayerVisible, (val) => {
vectorLayer.setVisible(val);
});
}); });
onUnmounted(() => { onUnmounted(() => {

View File

@ -0,0 +1,904 @@
<template>
<div class="layer-switcher">
<div class="layer-icon" @click="toggleLayerPanel"></div>
<!-- 图层面板 -->
<div class="layer-panel" v-if="showPanel">
<!-- 标签页切换 -->
<div class="panel-tabs">
<div
class="tab"
:class="{ active: activeTab === 'icon' }"
@click="activeTab = 'icon'"
>
图标
</div>
<!-- <div
class="tab"
:class="{ active: activeTab === 'text' }"
@click="activeTab = 'text'"
>
文本
</div> -->
<div
class="tab"
:class="{ active: activeTab === 'road' }"
@click="activeTab = 'road'"
>
道路
</div>
</div>
<div class="panel-content" v-if="activeTab === 'icon'">
<!-- 航空器图层 -->
<div class="layer-group">
<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">
<label class="checkbox-container">
<input type="checkbox" :checked="layer.visible" @change="toggleExampleLayer(layer)">
<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>
</label>
</div>
</div>
</div>
</div>
<div class="panel-content" v-else-if="activeTab === 'road'">
<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="!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>
</label>
</div>
<!-- 添加自定义路线图层选项 -->
<div class="layer-item">
<label class="checkbox-container">
<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>
</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>
<script setup>
import { ref, onMounted, watch, computed, onBeforeMount, onUnmounted } from 'vue';
import { Vector as VectorSource } from 'ol/source';
import { Vector as VectorLayer } from 'ol/layer';
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 car1Icon from '../../../assets/images/Aircraft.png';
import car2Icon from '../../../assets/images/noPeopleCar.png';
import noPeopleCarIcon from '../../../assets/images/noPeopleCar.png';
// props
const props = defineProps({
map: Object
});
//
const showPanel = ref(false);
const activeTab = ref('icon'); //
const selectedTextStyle = ref('white'); //
// tab
const hideRoadLayer = ref(false); //
const showRoadLayer = computed(() => !hideRoadLayer.value); // hideRoadLayer
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']);
//
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;
console.log('toggleHideRoadLayer: hideRoadLayer =', hideRoadLayer.value, 'showRoadLayer =', showRoadLayer.value);
if (showRoadLayer.value) {
addRoadLayer();
} else {
removeRoadLayer();
}
}
//
onMounted(() => {
if (props.map) {
initLayers();
// 线
loadCustomRoadLayer();
// -
if (showRoadLayer.value) {
addRoadLayer();
}
}
});
//
watch(() => props.map, (newMap) => {
if (newMap) {
initLayers();
// 线
loadCustomRoadLayer();
//
if (showRoadLayer.value) {
addRoadLayer();
}
}
});
//
defineExpose({
setLayerVisibility
});
async function loadCustomRoadLayer() {
// 线
removeCustomRoadLayer();
if (!props.map) return;
try {
// 使线
const res = await fetch('./roadTest.json');
const geojson = await res.json();
const source = new VectorSource({
features: new GeoJSON().readFeatures(geojson, {
dataProjection: 'EPSG:4326',
featureProjection: props.map.getView().getProjection()
})
});
customRoadVectorLayer = new VectorLayer({
source,
style: new Style({
stroke: new Stroke({ color: '#888', width: 4 })
}),
zIndex: 2, //
visible: showCustomRoadLayer.value
});
props.map.addLayer(customRoadVectorLayer);
console.log('loadCustomRoadLayer: 已添加自定义路线图层', customRoadVectorLayer);
} catch (e) {
console.error('loadCustomRoadLayer: 加载或添加自定义路线图层失败', e);
}
}
function removeCustomRoadLayer() {
if (!props.map) return;
let removed = false;
// VectorLayer zIndex 2 线
const layers = props.map.getLayers().getArray();
for (let i = layers.length - 1; i >= 0; i--) {
const lyr = layers[i];
if (lyr instanceof VectorLayer && lyr.getZIndex && lyr.getZIndex() === 2) {
props.map.removeLayer(lyr);
removed = true;
console.log('removeCustomRoadLayer: 已移除自定义路线图层', lyr);
}
}
customRoadVectorLayer = null;
if (!removed) {
console.log('removeCustomRoadLayer: 没有找到可移除的自定义路线图层');
}
}
// 线
watch(showCustomRoadLayer, (val) => {
console.log('showCustomRoadLayer变化:', val);
if (val) {
if (customRoadVectorLayer) {
customRoadVectorLayer.setVisible(true);
} else {
loadCustomRoadLayer();
}
} else if (customRoadVectorLayer) {
customRoadVectorLayer.setVisible(false);
}
});
async function addRoadLayer() {
//
removeRoadLayer();
if (!props.map) return;
try {
// 使
const res = await fetch('./dianziweilan.json');
const geojson = await res.json();
const source = new VectorSource({
features: new GeoJSON().readFeatures(geojson, {
dataProjection: 'EPSG:4326',
featureProjection: props.map.getView().getProjection()
})
});
// -
const normalStyle = new Style({
stroke: new Stroke({ color: 'rgba(0, 0, 0, 0.4)', width: 4 }),
fill: new Fill({ color: 'rgba(0, 0, 0, 0.5)' })
});
const flashStyle = new Style({
stroke: new Stroke({ color: 'rgba(255, 0, 0, 0.8)', width: 4 }),
fill: new Fill({ color: 'rgba(255, 0, 0, 0.3)' })
});
roadVectorLayer = new VectorLayer({
source,
style: normalStyle,
zIndex: 1,
className: 'fence-layer'
});
props.map.addLayer(roadVectorLayer);
console.log('addRoadLayer: 已添加电子围栏图层', roadVectorLayer);
//
startFenceFlashing(roadVectorLayer, normalStyle, flashStyle);
} catch (e) {
console.error('addRoadLayer: 加载或添加电子围栏图层失败', e);
}
}
let flashingInterval = null;
//
function startFenceFlashing(layer, normalStyle, flashStyle) {
//
if (flashingInterval) {
clearInterval(flashingInterval);
}
//
let isFlashing = false;
//
flashingInterval = setInterval(() => {
if (!layer) return;
layer.setStyle(isFlashing ? normalStyle : flashStyle);
isFlashing = !isFlashing;
}, 800); // 800
}
//
function stopFenceFlashing() {
if (flashingInterval) {
clearInterval(flashingInterval);
flashingInterval = null;
}
}
//
function removeRoadLayer() {
if (!props.map) return;
stopFenceFlashing();
let removed = false;
// VectorLayer zIndex 1
const layers = props.map.getLayers().getArray();
for (let i = layers.length - 1; i >= 0; i--) {
const lyr = layers[i];
if (lyr instanceof VectorLayer && lyr.getZIndex && lyr.getZIndex() === 1) {
props.map.removeLayer(lyr);
removed = true;
console.log('removeRoadLayer: 已移除道路图层', lyr);
}
}
roadVectorLayer = null;
if (!removed) {
console.log('removeRoadLayer: 没有找到可移除的道路图层');
}
}
watch(showRoadLayer, (val) => {
console.log('showRoadLayer变化:', val);
if (val) addRoadLayer();
else removeRoadLayer();
});
onUnmounted(() => {
removeRoadLayer();
removeCustomRoadLayer();
stopFenceFlashing();
});
</script>
<style scoped>
.layer-switcher {
position: relative;
}
.layer-icon {
width: 40px;
height: 40px;
cursor: pointer;
}
.layer-panel {
position: absolute;
top: 10px;
left: 50px;
width:397px;
background-color: #424851;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0,0,0,0.4);
z-index: 3100;
overflow: hidden;
color: #ffffff;
}
.panel-tabs {
display: flex;
border-bottom: 1px solid #303850;
}
.tab {
padding: 0 20px;
height: 40px;
line-height: 40px;
font-size: 14px;
text-align: center;
cursor: pointer;
transition: all 0.2s ease;
color: #F0F0F0;
position: relative;
}
.tab:hover {
color: #409eff;
}
.tab.active {
color: #409eff;
}
.tab.active::after {
content: "";
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 100%;
height: 2px;
background-color: #409eff;
}
.panel-content {
padding: 10px;
max-height: 400px;
overflow-y: auto;
}
.layer-group {
margin-bottom: 15px;
}
.group-title {
font-weight: bold;
margin-bottom: 8px;
padding-bottom: 5px;
border-bottom: 1px solid #999999;
color: #ffffff;
}
.layer-grid {
display: grid;
grid-template-columns: auto auto auto;
gap: 5px;
}
.layer-grid-full {
display: grid;
grid-template-columns: 1fr;
gap: 5px;
}
.layer-item {
padding: 6px 0;
}
/* 自定义复选框样式 */
.checkbox-container {
display: flex;
align-items: center;
position: relative;
padding-left: 30px;
cursor: pointer;
font-size: 14px;
user-select: none;
}
.checkbox-container input {
position: absolute;
opacity: 0;
cursor: pointer;
height: 0;
width: 0;
}
.checkmark {
position: absolute;
left: 0;
height: 18px;
width: 18px;
background-color: transparent;
border: 1px solid #999999;
border-radius: 3px;
}
.checkbox-container:hover input ~ .checkmark {
background-color: #666666;
}
.checkbox-container input:checked ~ .checkmark {
background-color: #0096ff;
border-color: #0078cc;
}
.checkmark:after {
content: "";
position: absolute;
display: none;
}
.checkbox-container input:checked ~ .checkmark:after {
display: block;
}
.checkbox-container .checkmark:after {
left: 6px;
top: 2px;
width: 5px;
height: 10px;
border: solid white;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
.layer-name {
margin-left: 5px;
color: #ffffff;
}
/* 图层图标预览 */
.layer-icon-preview {
width: 24px;
height: 24px;
margin-left: 8px;
object-fit: contain;
}
/* 滚动条样式 */
.panel-content::-webkit-scrollbar {
width: 6px;
}
.panel-content::-webkit-scrollbar-track {
background: #555555;
}
.panel-content::-webkit-scrollbar-thumb {
background: #999999;
border-radius: 3px;
}
.panel-content::-webkit-scrollbar-thumb:hover {
background: #bbbbbb;
}
/* 文本样式选择器 */
.style-selector {
display: flex;
flex-direction: column;
gap: 15px;
padding: 10px 0;
}
.style-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 5px 10px;
}
.style-label {
font-size: 14px;
color: #ffffff;
}
.radio-box {
width: 20px;
height: 20px;
border: 2px solid #999999;
border-radius: 3px;
cursor: pointer;
position: relative;
}
.radio-box:hover {
border-color: #bbbbbb;
}
.radio-box.active {
border-color: #0096ff;
}
.radio-box.active::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 12px;
height: 12px;
background-color: #0096ff;
border-radius: 1px;
}
.radio-box.blue {
border-color: #0096ff;
}
.radio-box.blue.active::after {
background-color: #0096ff;
}
.radio-box.white {
border-color: #ffffff;
}
.radio-box.white.active::after {
background-color: #ffffff;
}
/* 道路json样式 */
.road-json {
padding: 10px;
background-color: #333;
border-radius: 4px;
margin-top: 10px;
}
.road-json pre {
margin: 0;
padding: 0;
white-space: pre-wrap;
word-break: break-all;
}
</style>

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="map-info"> <div class="map-info">
<input type="text" readonly :value="xyText" /> <!-- <input type="text" readonly :value="xyText" />
<input type="text" readonly :value="wgs84Text" /> <input type="text" readonly :value="wgs84Text" /> -->
<!-- <input type="text" readonly :value="pixelText" /> --> <!-- <input type="text" readonly :value="pixelText" /> -->
</div> </div>
</template> </template>
@ -39,7 +39,7 @@ function handleMouseMove(event) {
// 3. WGS84 // 3. WGS84
try { try {
const [lng, lat] = transform(coordinate, 'EPSG:4528', 'EPSG:4326'); const [lng, lat] = transform(coordinate, 'EPSG:4528', 'EPSG:4326');
//console.log('WGS84:', lng, lat); // console.log('WGS84:', lng, lat);
wgs84Text.value = `WGS84: 经度=${lng.toFixed(6)}°, 纬度=${lat.toFixed(6)}°`; wgs84Text.value = `WGS84: 经度=${lng.toFixed(6)}°, 纬度=${lat.toFixed(6)}°`;
} catch (error) { } catch (error) {
// console.error(':', error); // console.error(':', error);

View File

@ -1,25 +1,7 @@
<template> <template>
<div class="zoom-control"> <div class="zoom-control">
<el-popover <LayerSwitcher class="layer-switcher" :map="map" @layerChange="onLayerChange" ref="layerSwitcherRef" />
placement="right"
width="160"
trigger="click"
v-model:visible="popoverVisible"
popper-class="layer-popover"
>
<el-checkbox
:model-value="props.routeLayerVisible"
@change="onLayerToggle"
>道路图层</el-checkbox>
<template #reference>
<div
class="layer-toggle"
:class="{ active: props.routeLayerVisible }"
@click="handleLayerBtnClick"
title="图层控制"
></div>
</template>
</el-popover>
<div class="compass" @click="onCompass"></div> <div class="compass" @click="onCompass"></div>
<div class="zoom-in" @click="onZoomIn"></div> <div class="zoom-in" @click="onZoomIn"></div>
<div class="zoom-out" @click="onZoomOut"></div> <div class="zoom-out" @click="onZoomOut"></div>
@ -28,8 +10,7 @@
<script setup> <script setup>
import { ref, onMounted } from 'vue'; import { ref, onMounted } from 'vue';
import { ElPopover, ElCheckbox } from 'element-plus'; import LayerSwitcher from './LayerSwitcher.vue';
// import LayerSwitcher from './LayerSwitcher.vue';
// props // props
const props = defineProps({ const props = defineProps({
@ -37,10 +18,6 @@ const props = defineProps({
resetView: { resetView: {
type: Function, type: Function,
default: null default: null
},
routeLayerVisible: {
type: Boolean,
default: false
} }
}); });
@ -48,9 +25,7 @@ const props = defineProps({
const layerSwitcherRef = ref(null); const layerSwitcherRef = ref(null);
// //
const emit = defineEmits(['compass', 'zoomIn', 'zoomOut', 'layerChange', 'layerToggle']); const emit = defineEmits(['compass', 'zoomIn', 'zoomOut', 'layerChange']);
const popoverVisible = ref(false);
// / // /
function onCompass() { function onCompass() {
@ -82,14 +57,6 @@ function onLayerChange(layerInfo) {
emit('layerChange', layerInfo); emit('layerChange', layerInfo);
} }
function onLayerToggle() {
emit('layerToggle');
}
function handleLayerBtnClick() {
popoverVisible.value = !popoverVisible.value;
}
// //
defineExpose({ defineExpose({
// //
@ -116,22 +83,6 @@ defineExpose({
gap: 25px; gap: 25px;
} }
.layer-toggle {
width: 40px;
height: 40px;
background: url("../../../assets/images/layer.png") no-repeat;
background-size: 100% 100%;
cursor: pointer;
z-index: 3000;
border-radius: 8px;
border: 2px solid transparent;
box-sizing: border-box;
transition: border 0.2s;
}
.layer-toggle.active {
border: 2px solid #409eff;
box-shadow: 0 0 8px #409eff44;
}
.compass { .compass {
width: 40px; width: 40px;
height: 40px; height: 40px;
@ -140,6 +91,15 @@ defineExpose({
cursor: pointer; cursor: pointer;
z-index: 3000; z-index: 3000;
} }
.layer-switcher{
z-index: 3000;
width: 40px;
height: 40px;
background: url("../../../assets/images/layer.png") no-repeat;
background-size: 100% 100%;
cursor: pointer;
}
.zoom-in { .zoom-in {
background: url("../../../assets/images/zoomOut.png") no-repeat; background: url("../../../assets/images/zoomOut.png") no-repeat;
background-size: 100% 100%; background-size: 100% 100%;
@ -148,6 +108,7 @@ defineExpose({
height: 40px; height: 40px;
cursor: pointer; cursor: pointer;
} }
.zoom-out { .zoom-out {
background: url("../../../assets/images/zoomIn.png") no-repeat; background: url("../../../assets/images/zoomIn.png") no-repeat;
background-size: 100% 100%; background-size: 100% 100%;
@ -159,7 +120,7 @@ defineExpose({
/* 响应式调整 */ /* 响应式调整 */
@media (max-width: 768px) { @media (max-width: 768px) {
.zoom-in, .zoom-out, .compass, .layer-toggle { .zoom-in, .zoom-out, .compass {
width: 28px; width: 28px;
height: 28px; height: 28px;
background-size: 80%; background-size: 80%;
@ -167,9 +128,4 @@ defineExpose({
background-repeat: no-repeat; background-repeat: no-repeat;
} }
} }
.layer-popover {
padding: 12px 16px;
min-width: 120px;
}
</style> </style>

View File

@ -0,0 +1,712 @@
<template>
<div class="route-draw-control">
<div class="draw-tools">
<div class="tool-button" @click="startDrawLine" :class="{ active: isDrawing && drawType === 'LineString' }">
<span>绘制路线</span>
</div>
<div class="tool-button" @click="startDrawPolygon" :class="{ active: isDrawing && drawType === 'Polygon' }">
<span>绘制点线面</span>
</div>
<div class="tool-button" @click="clearDraw">
<span>清除</span>
</div>
<div class="tool-button" @click="exportRouteData">
<span>导出数据</span>
</div>
<div class="tool-button" @click="showImportDialog = true">
<span>导入数据</span>
</div>
</div>
<!-- 右侧点线面信息展示面板 -->
<div class="polygon-panel" v-if="showPolygonPanel">
<div class="panel-header">
<span>点线面信息</span>
<span class="close-btn" @click="closePolygonPanel">×</span>
</div>
<div class="panel-content">
<div class="info-item" v-for="(polygon, index) in polygons" :key="index">
<div class="info-header">点线面 {{ index + 1 }}</div>
<div class="info-content">
<p>面积: {{ polygon.area.toFixed(2) }} 平方米</p>
<p>周长: {{ polygon.perimeter.toFixed(2) }} </p>
<p>顶点数: {{ polygon.vertices }}</p>
<button class="focus-btn" @click="focusOnPolygon(polygon)">定位</button>
</div>
</div>
<div class="no-data" v-if="polygons.length === 0">
暂无点线面数据请先绘制点线面
</div>
</div>
</div>
<!-- 路线信息面板 -->
<div class="route-lines-panel" v-if="showRouteLinesPanel">
<div class="panel-header">
<span>路线信息</span>
<span class="close-btn" @click="closeRouteLinesPanel">×</span>
</div>
<div class="panel-content">
<div class="info-item" v-for="(line, index) in routeLines" :key="index">
<div class="info-header">路线 {{ index + 1 }}</div>
<div class="info-content">
<p>长度: {{ line.length.toFixed(2) }} </p>
<p>顶点数: {{ line.vertices }}</p>
<button class="focus-btn" @click="focusOnRoute(line)">定位</button>
</div>
</div>
<div class="no-data" v-if="routeLines.length === 0">
暂无路线数据请先绘制路线
</div>
</div>
</div>
<!-- 导入JSON对话框 -->
<div class="import-dialog" v-if="showImportDialog">
<div class="dialog-content">
<div class="dialog-header">
<h3>导入数据</h3>
<span class="close-btn" @click="showImportDialog = false">×</span>
</div>
<div class="dialog-body">
<textarea v-model="importJsonText" placeholder="请粘贴JSON数据"></textarea>
<div class="file-upload">
<label for="jsonFileInput">或者上传JSON文件</label>
<input type="file" id="jsonFileInput" accept=".json" @change="handleFileUpload">
</div>
</div>
<div class="dialog-footer">
<button @click="showImportDialog = false">取消</button>
<button @click="importRouteData" class="primary-btn">导入</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, watch } from 'vue';
import Draw from 'ol/interaction/Draw';
import { Vector as VectorSource } from 'ol/source';
import { Vector as VectorLayer } from 'ol/layer';
import { Style, Stroke, Fill, Circle } from 'ol/style';
import { getArea, getLength } from 'ol/sphere';
import { LineString, Polygon } from 'ol/geom';
import GeoJSON from 'ol/format/GeoJSON';
import Feature from 'ol/Feature';
// props
const props = defineProps({
map: Object
});
//
const isDrawing = ref(false);
const drawType = ref('');
const showPolygonPanel = ref(false);
const showRouteLinesPanel = ref(false);
const polygons = ref([]);
const routeLines = ref([]);
const showImportDialog = ref(false);
const importJsonText = ref('');
//
let drawInteraction = null;
let vectorSource = null;
let vectorLayer = null;
//
function initDrawLayer() {
if (!props.map) return;
//
vectorSource = new VectorSource();
//
vectorLayer = new VectorLayer({
source: vectorSource,
style: new Style({
fill: new Fill({
color: 'rgba(0, 150, 255, 0.3)'
}),
stroke: new Stroke({
color: '#0096ff',
width: 3
}),
image: new Circle({
radius: 7,
fill: new Fill({
color: '#0096ff'
})
})
})
});
//
props.map.addLayer(vectorLayer);
}
// 线
function startDrawLine() {
clearDraw();
isDrawing.value = true;
drawType.value = 'LineString';
showRouteLinesPanel.value = true;
drawInteraction = new Draw({
source: vectorSource,
type: 'LineString',
style: new Style({
stroke: new Stroke({
color: '#ff0000',
width: 3
}),
image: new Circle({
radius: 5,
fill: new Fill({
color: '#ff0000'
})
})
})
});
//
drawInteraction.on('drawend', function(event) {
const feature = event.feature;
const geometry = feature.getGeometry();
// 线
const length = getLength(geometry);
const vertices = geometry.getCoordinates().length;
//
feature.setProperties({
type: 'route',
length: length,
vertices: vertices
});
// 线
routeLines.value.push({
id: Date.now(),
feature: feature,
length: length,
vertices: vertices,
extent: geometry.getExtent()
});
isDrawing.value = false;
});
props.map.addInteraction(drawInteraction);
}
// 线
function startDrawPolygon() {
clearDraw();
isDrawing.value = true;
drawType.value = 'Polygon';
showPolygonPanel.value = true;
drawInteraction = new Draw({
source: vectorSource,
type: 'Polygon',
style: new Style({
fill: new Fill({
color: 'rgba(255, 165, 0, 0.3)'
}),
stroke: new Stroke({
color: '#ffa500',
width: 3
}),
image: new Circle({
radius: 5,
fill: new Fill({
color: '#ffa500'
})
})
})
});
//
drawInteraction.on('drawend', function(event) {
const feature = event.feature;
const geometry = feature.getGeometry();
//
const area = getArea(geometry);
const perimeter = getLength(geometry.getLinearRing(0));
const vertices = geometry.getLinearRing(0).getCoordinates().length - 1; //
//
feature.setProperties({
type: 'polygon',
area: area,
perimeter: perimeter,
vertices: vertices
});
// 线
polygons.value.push({
id: Date.now(),
feature: feature,
area: area,
perimeter: perimeter,
vertices: vertices,
extent: geometry.getExtent()
});
isDrawing.value = false;
});
props.map.addInteraction(drawInteraction);
}
//
function clearDraw() {
if (drawInteraction) {
props.map.removeInteraction(drawInteraction);
drawInteraction = null;
}
if (vectorSource) {
vectorSource.clear();
}
isDrawing.value = false;
polygons.value = [];
routeLines.value = [];
}
// 线
function closePolygonPanel() {
showPolygonPanel.value = false;
}
// 线
function closeRouteLinesPanel() {
showRouteLinesPanel.value = false;
}
// 线
function focusOnPolygon(polygon) {
if (props.map && polygon.extent) {
props.map.getView().fit(polygon.extent, {
padding: [50, 50, 50, 50],
duration: 1000
});
}
}
// 线
function focusOnRoute(route) {
if (props.map && route.extent) {
props.map.getView().fit(route.extent, {
padding: [50, 50, 50, 50],
duration: 1000
});
}
}
// 线GeoJSON
function exportRouteData() {
if (!vectorSource || vectorSource.getFeatures().length === 0) {
alert('没有可导出的数据,请先绘制路线或点线面');
return;
}
// GeoJSON
const format = new GeoJSON();
//
const features = vectorSource.getFeatures();
// GeoJSON
const geojsonObj = format.writeFeaturesObject(features, {
dataProjection: 'EPSG:4326', // WGS84
featureProjection: props.map.getView().getProjection() //
});
// JSON
const geojsonStr = JSON.stringify(geojsonObj, null, 2);
//
const blob = new Blob([geojsonStr], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'roadTest.json'; // roadTest.json
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
console.log('数据已导出为GeoJSON文件名roadTest.json');
alert('文件已下载为roadTest.json请将其复制到项目的public目录下');
}
//
function handleFileUpload(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
importJsonText.value = e.target.result;
};
reader.readAsText(file);
}
// JSON线
function importRouteData() {
if (!importJsonText.value) {
alert('请输入或上传JSON数据');
return;
}
try {
// JSON
const geojsonObj = JSON.parse(importJsonText.value);
// GeoJSON
const format = new GeoJSON();
//
clearDraw();
// GeoJSON
const features = format.readFeatures(geojsonObj, {
dataProjection: 'EPSG:4326', // WGS84
featureProjection: props.map.getView().getProjection() //
});
//
vectorSource.addFeatures(features);
// 线线
features.forEach(feature => {
const geometry = feature.getGeometry();
const props = feature.getProperties();
if (geometry instanceof LineString || props.type === 'route') {
// 线
const length = getLength(geometry);
const vertices = geometry.getCoordinates().length;
routeLines.value.push({
id: Date.now() + Math.random(),
feature: feature,
length: props.length || length,
vertices: props.vertices || vertices,
extent: geometry.getExtent()
});
} else if (geometry instanceof Polygon || props.type === 'polygon') {
// 线
const area = getArea(geometry);
const perimeter = getLength(geometry.getLinearRing(0));
const vertices = geometry.getLinearRing(0).getCoordinates().length - 1;
polygons.value.push({
id: Date.now() + Math.random(),
feature: feature,
area: props.area || area,
perimeter: props.perimeter || perimeter,
vertices: props.vertices || vertices,
extent: geometry.getExtent()
});
}
});
//
if (routeLines.value.length > 0) {
showRouteLinesPanel.value = true;
}
if (polygons.value.length > 0) {
showPolygonPanel.value = true;
}
//
showImportDialog.value = false;
importJsonText.value = '';
//
if (features.length > 0) {
const extent = vectorSource.getExtent();
props.map.getView().fit(extent, {
padding: [50, 50, 50, 50],
duration: 1000
});
}
console.log('成功导入数据,共', features.length, '个特征');
} catch (error) {
console.error('导入数据失败:', error);
alert('导入失败请检查JSON格式是否正确');
}
}
//
onMounted(() => {
if (props.map) {
initDrawLayer();
}
});
//
watch(() => props.map, (newMap) => {
if (newMap) {
initDrawLayer();
}
});
//
onUnmounted(() => {
if (props.map) {
if (drawInteraction) {
props.map.removeInteraction(drawInteraction);
}
if (vectorLayer) {
props.map.removeLayer(vectorLayer);
}
}
});
//
defineExpose({
startDrawLine,
startDrawPolygon,
clearDraw,
exportRouteData,
importRouteData,
showImportDialog
});
</script>
<style scoped>
.route-draw-control {
position: relative;
}
.draw-tools {
position: absolute;
top: 20px;
left: 20px;
z-index: 3000;
background-color: white;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
padding: 10px;
display: flex;
flex-direction: column;
gap: 8px;
min-width: 120px;
border: 2px solid #0096ff;
}
.tool-button {
padding: 8px 12px;
background-color: #f5f5f5;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
text-align: center;
transition: all 0.2s;
font-weight: bold;
color: #333;
}
.tool-button:hover {
background-color: #e0e0e0;
transform: scale(1.05);
}
.tool-button.active {
background-color: #0096ff;
color: white;
border-color: #0078cc;
}
.polygon-panel, .route-lines-panel {
position: absolute;
top: 20px;
right: 20px;
width: 300px;
max-height: 80vh;
background-color: white;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
z-index: 3000;
display: flex;
flex-direction: column;
overflow: hidden;
}
.route-lines-panel {
right: 340px;
}
.panel-header {
padding: 12px 15px;
background-color: #f5f5f5;
border-bottom: 1px solid #ddd;
font-weight: bold;
display: flex;
justify-content: space-between;
align-items: center;
}
.close-btn {
cursor: pointer;
font-size: 20px;
color: #666;
}
.close-btn:hover {
color: #333;
}
.panel-content {
padding: 15px;
overflow-y: auto;
max-height: calc(80vh - 50px);
}
.info-item {
margin-bottom: 15px;
border: 1px solid #eee;
border-radius: 4px;
overflow: hidden;
}
.info-header {
padding: 8px 12px;
background-color: #f9f9f9;
border-bottom: 1px solid #eee;
font-weight: bold;
}
.info-content {
padding: 12px;
}
.info-content p {
margin: 5px 0;
font-size: 14px;
}
.focus-btn {
margin-top: 8px;
padding: 5px 10px;
background-color: #0096ff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.focus-btn:hover {
background-color: #0078cc;
}
.no-data {
text-align: center;
color: #999;
padding: 20px 0;
}
/* 导入对话框样式 */
.import-dialog {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 5000;
}
.dialog-content {
width: 500px;
background-color: white;
border-radius: 4px;
overflow: hidden;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
}
.dialog-header {
padding: 15px;
background-color: #f5f5f5;
border-bottom: 1px solid #ddd;
display: flex;
justify-content: space-between;
align-items: center;
}
.dialog-header h3 {
margin: 0;
font-size: 18px;
}
.dialog-body {
padding: 20px;
}
.dialog-body textarea {
width: 100%;
height: 200px;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
resize: none;
font-family: monospace;
margin-bottom: 15px;
}
.file-upload {
margin-top: 10px;
}
.file-upload label {
display: inline-block;
padding: 8px 12px;
background-color: #f5f5f5;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
}
.file-upload input {
display: none;
}
.dialog-footer {
padding: 15px;
background-color: #f5f5f5;
border-top: 1px solid #ddd;
display: flex;
justify-content: flex-end;
gap: 10px;
}
.dialog-footer button {
padding: 8px 15px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #f5f5f5;
cursor: pointer;
}
.dialog-footer button.primary-btn {
background-color: #0096ff;
color: white;
border-color: #0078cc;
}
.dialog-footer button.primary-btn:hover {
background-color: #0078cc;
}
</style>

View File

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

View File

@ -1,7 +1,11 @@
<template> <template>
<div class="event-list-container"> <div class="event-list-container">
<div class="event-list-header"> <div class="event-list-header">
<div class="header-title">车辆列表 ({{ totalCount }})</div> <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="header-actions">
<div class="filter-dropdown"> <div class="filter-dropdown">
<span class="selected-filter">全部</span> <span class="selected-filter">全部</span>
@ -181,10 +185,9 @@ onMounted(() => {
}); });
</script> </script>
<style scoped> <style scoped lang="scss">
.event-list-container { .event-list-container {
background: url("../../../assets/images/list_bg.png") no-repeat; background-color: #27313F;
background-size: 100% 100%;
position: absolute; position: absolute;
right: 20px; right: 20px;
top: 50%; top: 50%;
@ -203,21 +206,27 @@ onMounted(() => {
.header-title { .header-title {
display: flex;
align-items: center;
font-size: 16px; font-size: 16px;
line-height: 1.8; line-height: 1.8;
font-weight: bold; font-weight: bold;
margin-left: 35px; background:url("../../../assets/images/subheading.png") no-repeat;
padding:0 10px; background-size:100% 100%;
padding:5px 10px;
color: #fff; color: #fff;
img{
width: 31px;
height: 15px;
margin-right: 5px;
}
} }
.header-actions { .header-actions {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding:0 16px; padding:0 16px;
margin: 20px 0; margin: 10px 0;
} }
.filter-dropdown { .filter-dropdown {

View File

@ -13,20 +13,23 @@
</div> </div>
<img src="../../assets/images/close.png" alt="close" class="close-icon" /> <img src="../../assets/images/close.png" alt="close" class="close-icon" @click="handleLogout" />
</div> </div>
</template> </template>
<script setup> <script setup>
import { computed } from "vue"; import { computed } from "vue";
import { ElMessageBox } from 'element-plus'
import { useRouter, useRoute } from "vue-router"; import { useRouter, useRoute } from "vue-router";
import useAppStore from '@/store/modules/app'; import useAppStore from '@/store/modules/app';
import usePermissionStore from '@/store/modules/permission'; import usePermissionStore from '@/store/modules/permission';
import useUserStore from '@/store/modules/user'
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
const appStore = useAppStore(); const appStore = useAppStore();
const userStore = useUserStore()
const permissionStore = usePermissionStore(); const permissionStore = usePermissionStore();
// Tab // Tab
@ -70,6 +73,19 @@ function handleTabClick(path) {
router.push(path); router.push(path);
} }
} }
// 退
function handleLogout() {
ElMessageBox.confirm('确定注销并退出系统吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
userStore.logOut().then(() => {
router.push('/login');
});
}).catch(() => {});
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -324,7 +324,7 @@ const cardList = [
:deep(.el-tabs__item.is-active) { :deep(.el-tabs__item.is-active) {
color: #409eff; color: #409eff;
} }
::v-deep .el-tabs__nav-wrap::after { :deep .el-tabs__nav-wrap::after {
height: 1px; height: 1px;
background-color: #4C4F5F; background-color: #4C4F5F;
} }

View File

@ -1,8 +1,7 @@
<template> <template>
<div class="app-container"> <div class="app-container">
<div class="park-container"> <div class="park-container">
<!-- 车辆状态统计卡片 -->
<VehicleStats :stats="vehicleStats" />
<!-- 搜索区域 --> <!-- 搜索区域 -->
<div class="search-area"> <div class="search-area">
@ -34,7 +33,10 @@
</div> </div>
<div class="action-buttons"> <div class="action-buttons">
<el-button class="export-btn" @click="handleExport">导出</el-button> <el-button class="search-btn" type="primary" @click="handleAdd">新增</el-button>
<el-button class="reset-btn" @click="handleImport">导入</el-button>
<el-button class="reset-btn" @click="handleExport">导出</el-button>
<el-button class="export-btn" @click="handleDelete" :disabled="multiple">删除</el-button>
</div> </div>
</div> </div>
@ -56,34 +58,22 @@
</el-table-column> </el-table-column>
<el-table-column label="操作" width="120" align="left"> <el-table-column label="操作" width="120" align="left">
<template #default="scope"> <template #default="scope">
<el-button link text @click="handleView(scope.row)">查看</el-button> <el-button link text @click="handleEdit(scope.row)">编辑</el-button>
<el-button link text type="danger" @click="handleDelete(scope.row)">删除</el-button>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="车辆号" prop="carId" align="left" /> <el-table-column label="车牌号" prop="carNo" align="left" />
<el-table-column label="类型" prop="type" align="left" /> <el-table-column label="类型" prop="type" align="left" />
<el-table-column label="品牌" prop="brand" align="left" /> <el-table-column label="品牌" prop="brand" align="left" />
<el-table-column label="所属单位" prop="organization" align="left" /> <el-table-column label="所属单位" prop="organization" align="left" />
<el-table-column label="当前故障数" prop="faultCount" align="left" />
<el-table-column label="作业状态" align="left">
<template #default="scope">
<el-tag :type="getRouteStatusType(scope.row.routeStatus)">
{{ scope.row.routeStatus }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="车辆状态" align="left">
<template #default="scope">
<el-tag :type="getStatusType(scope.row.status)">
{{ scope.row.status }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="充电状态" prop="chargeStatus" align="left" />
<el-table-column label="电池电量" prop="batteryLevel" align="left" />
<el-table-column label="最新位置" prop="location" align="left" />
<el-table-column label="最新时间" prop="lastUpdateTime" align="left" width="160" />
<el-table-column label="负责人" prop="contact" align="left" /> <el-table-column label="负责人" prop="contact" align="left" />
<el-table-column label="负责人电话" prop="phonenumber" 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> </el-table>
<!-- 分页组件 --> <!-- 分页组件 -->
@ -95,14 +85,138 @@
@pagination="getList" @pagination="getList"
/> />
</div> </div>
<!-- 新增/编辑弹窗 -->
<el-dialog
v-model="dialog.visible"
:title="dialog.title"
width="600px"
append-to-body
class="vehicle-dialog"
>
<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>
</el-col>
<el-col :span="24">
<el-form-item label="车辆类型" prop="type">
<el-input v-model="form.type" placeholder="请输入车辆类型" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="车辆品牌" prop="brand">
<el-input v-model="form.brand" placeholder="请输入车辆品牌" />
</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>
</el-col>
<el-col :span="24">
<el-form-item label="联系人" prop="contact">
<el-input v-model="form.contact" 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>
</el-col>
</el-row>
<el-form-item label="车辆图片" prop="image">
<div class="avatar-uploader-box">
<div v-if="form.image" class="avatar-preview">
<img :src="form.image" class="avatar" />
<div class="avatar-replace" @click.stop="handleRemoveImage">
<el-icon><Close /></el-icon>
</div>
</div>
<el-upload
v-else
class="avatar-uploader"
:action="uploadUrl"
:headers="headers"
:show-file-list="false"
:on-success="handleImageSuccess"
:before-upload="beforeImageUpload"
>
<div class="avatar-upload-placeholder">
<el-icon class="avatar-uploader-icon"><Plus /></el-icon>
</div>
</el-upload>
<div class="avatar-count">{{ form.image ? '1/1' : '0/1' }}</div>
</div>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button class="export-btn" @click="dialog.visible = false"> </el-button>
<el-button class="search-btn" type="primary" @click="submitForm"> </el-button>
</div>
</template>
</el-dialog>
<!-- 导入弹窗 -->
<el-dialog
v-model="importDialog.visible"
title="导入车辆信息"
width="500px"
append-to-body
class="import-dialog"
>
<div class="import-title">选择需要导入的车辆文件</div>
<el-upload
ref="uploadRef"
:limit="1"
accept=".xlsx, .xls"
:headers="upload.headers"
:action="upload.url + '?updateSupport=' + upload.updateSupport"
:disabled="upload.isUploading"
:on-progress="handleFileUploadProgress"
:on-success="handleFileSuccess"
:auto-upload="false"
drag
class="import-upload"
>
<div class="upload-content">
<el-icon class="upload-icon"><upload-filled /></el-icon>
<div class="upload-text">拖拽文件到这里</div>
<div class="upload-link">
<el-icon class="upload-icon-small" style="color: #347AE2"><upload-filled /></el-icon>
<span class="click-upload">点此添加</span>
</div>
</div>
</el-upload>
<div class="import-footer">
<div class="footer-left">
<el-checkbox v-model="upload.updateSupport">是否更新已经存在的车辆数据</el-checkbox>
</div>
<div class="footer-right">
<span class="file-format">仅允许导入xlsxlsx格式文件</span>
<el-link type="primary" :underline="false" class="download-link" @click="importTemplate">下载模板</el-link>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button class="reset-btn" @click="importDialog.visible = false"> </el-button>
<el-button class="search-btn" type="primary" @click="submitFileForm"> </el-button>
</div>
</template>
</el-dialog>
</div> </div>
</template> </template>
<script setup name="VehicleMonitor"> <script setup name="VehiclePark">
import { ref, reactive, onMounted, computed } from 'vue'; import { ref, reactive, onMounted, getCurrentInstance } from 'vue';
import { getToken } from "@/utils/auth"; import { getToken } from "@/utils/auth";
import { parseTime } from "@/utils/ruoyi"; import { parseTime } from "@/utils/ruoyi";
import { Search, Download } from '@element-plus/icons-vue'; import { Search, Download, Plus, UploadFilled, Close } from '@element-plus/icons-vue';
import VehicleStats from '@/components/car/VehicleStats.vue'; import VehicleStats from '@/components/car/VehicleStats.vue';
import Pagination from '@/components/Pagination/index.vue'; import Pagination from '@/components/Pagination/index.vue';
@ -142,11 +256,68 @@ const queryParams = ref({
status: undefined status: undefined
}); });
// //
const dialog = reactive({
visible: false,
title: ''
});
const importDialog = reactive({
visible: false
});
//
const form = ref({
id: undefined,
carNo: '',
type: '',
brand: '',
organization: '',
contact: '',
phonenumber: '',
image: ''
});
//
const rules = {
carNo: [
{ required: true, message: "车牌号不能为空", trigger: "blur" }
],
type: [
{ required: true, message: "车辆类型不能为空", trigger: "change" }
],
brand: [
{ required: true, message: "车辆品牌不能为空", trigger: "blur" }
],
organization: [
{ required: true, message: "所属单位不能为空", trigger: "blur" }
],
contact: [
{ required: true, message: "联系人不能为空", trigger: "blur" }
],
phonenumber: [
{ required: true, message: "电话不能为空", trigger: "blur" }
]
};
//
const uploadUrl = import.meta.env.VITE_APP_BASE_API + "/common/upload";
const headers = { Authorization: "Bearer " + getToken() };
const upload = reactive({
isUploading: false,
updateSupport: 0,
title: "",
open: false,
// URL
url: import.meta.env.VITE_APP_BASE_API + "/car/park/importData",
headers: { Authorization: "Bearer " + getToken() }
});
//
const mockVehicleData = [ const mockVehicleData = [
{ {
id: 1, id: 1,
carId: 'QH001', carNo: '鲁B12345',
type: '驱动车', type: '驱动车',
brand: '大众', brand: '大众',
organization: '青岛机场物流公司', organization: '青岛机场物流公司',
@ -154,15 +325,15 @@ const mockVehicleData = [
routeStatus: '任务中', routeStatus: '任务中',
status: '在线', status: '在线',
chargeStatus: '未充电', chargeStatus: '未充电',
batteryLevel: '85%', batteryLevel: '89%',
location: '航站楼A区', location: '跑道区域A',
lastUpdateTime: '2024-04-17 10:48:30', lastUpdateTime: '2024-01-15 10:30:00',
contact: '张三', contact: '张三',
phonenumber: '12345678920' phonenumber: '13800138001'
}, },
{ {
id: 2, id: 2,
carId: 'QH002', carNo: '鲁B67890',
type: '巡检车', type: '巡检车',
brand: '金龙', brand: '金龙',
organization: '青岛机场运输公司', organization: '青岛机场运输公司',
@ -170,59 +341,59 @@ const mockVehicleData = [
routeStatus: '待命中', routeStatus: '待命中',
status: '在线', status: '在线',
chargeStatus: '未充电', chargeStatus: '未充电',
batteryLevel: '72%', batteryLevel: '65%',
location: '航站楼B区', location: '航站楼',
lastUpdateTime: '2024-04-17 10:48:30', lastUpdateTime: '2024-01-16 14:20:00',
contact: '李四', contact: '李四',
phonenumber: '12345678921' phonenumber: '13800138002'
}, },
{ {
id: 3, id: 3,
carId: 'QH003', carNo: '鲁B11111',
type: '驱动车', type: '驱动车',
brand: '解放', brand: '解放',
organization: '青岛机场货运部', organization: '青岛机场货运部',
faultCount: 2, faultCount: 3,
routeStatus: '充电中', routeStatus: '充电中',
status: '故障', status: '离线',
chargeStatus: '充电中', chargeStatus: '充电中',
batteryLevel: '45%', batteryLevel: '25%',
location: '充电站', location: '充电站',
lastUpdateTime: '2024-04-17 10:48:30', lastUpdateTime: '2024-01-17 09:15:00',
contact: '王五', contact: '王五',
phonenumber: '12345678922' phonenumber: '13800138003'
}, },
{ {
id: 4, id: 4,
carId: 'QH004', carNo: '鲁B22222',
type: '驱动车', type: '巡检车',
brand: '福特', brand: '宇通',
organization: '青岛机场物流公司', organization: '青岛机场安保部',
faultCount: 0, faultCount: 0,
routeStatus: '任务中', routeStatus: '等待中',
status: '在线', status: '故障',
chargeStatus: '未充电', chargeStatus: '未充电',
batteryLevel: '65%', batteryLevel: '12%',
location: '货运区', location: '货运区B',
lastUpdateTime: '2024-04-17 10:48:30', lastUpdateTime: '2024-01-17 16:40:00',
contact: '赵六', contact: '赵六',
phonenumber: '12345678923' phonenumber: '13800138004'
}, },
{ {
id: 5, id: 5,
carId: 'QH005', carNo: '鲁B33333',
type: '巡检车', type: '驱动车',
brand: '宇通', brand: '福田',
organization: '青岛机场运输公司', organization: '青岛机场维修部',
faultCount: 0, faultCount: 2,
routeStatus: '待命中', routeStatus: '待命中',
status: '线', status: '线',
chargeStatus: '未充电', chargeStatus: '未充电',
batteryLevel: '0%', batteryLevel: '78%',
location: '维修', location: '维修区C',
lastUpdateTime: '2024-04-17 10:48:30', lastUpdateTime: '2024-01-18 08:10:00',
contact: '钱七', contact: '钱七',
phonenumber: '12345678924' phonenumber: '13800138005'
} }
]; ];
@ -259,15 +430,13 @@ function getStatusType(status) {
/** 查询车辆列表 */ /** 查询车辆列表 */
function getList() { function getList() {
loading.value = true; loading.value = true;
// 使
setTimeout(() => { setTimeout(() => {
//
let filteredData = [...mockVehicleData]; let filteredData = [...mockVehicleData];
if (queryParams.value.carNo) { if (queryParams.value.carNo) {
const keyword = queryParams.value.carNo.toLowerCase(); const keyword = queryParams.value.carNo.toLowerCase();
filteredData = filteredData.filter(item => filteredData = filteredData.filter(item =>
item.carId.toLowerCase().includes(keyword) || item.carNo.toLowerCase().includes(keyword) ||
item.brand.toLowerCase().includes(keyword) || item.brand.toLowerCase().includes(keyword) ||
item.organization.toLowerCase().includes(keyword) item.organization.toLowerCase().includes(keyword)
); );
@ -284,12 +453,6 @@ function getList() {
loading.value = false; loading.value = false;
vehicleList.value = filteredData; vehicleList.value = filteredData;
total.value = filteredData.length; total.value = filteredData.length;
//
vehicleStats.value[0].count = mockVehicleData.filter(v => v.status === '在线').length;
vehicleStats.value[1].count = mockVehicleData.filter(v => v.routeStatus === '任务中').length;
vehicleStats.value[2].count = mockVehicleData.filter(v => v.status === '故障').length;
vehicleStats.value[3].count = mockVehicleData.filter(v => v.status === '离线').length;
}, 300); }, 300);
} }
@ -318,18 +481,137 @@ function handleSelectionChange(selection) {
multiple.value = !selection.length; multiple.value = !selection.length;
} }
/** 查看详情按钮操作 */ /** 新增按钮操作 */
function handleView(row) { function handleAdd() {
proxy.$modal.msgSuccess("查看车辆详情功能已模拟"); reset();
dialog.visible = true;
dialog.title = "添加车辆";
}
/** 修改按钮操作 */
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 = "修改车辆";
}
/** 提交按钮 */
function submitForm() {
proxy.$refs["vehicleFormRef"].validate(valid => {
if (valid) {
if (form.value.id != null) {
//
proxy.$modal.msgSuccess("修改成功");
} else {
//
proxy.$modal.msgSuccess("新增成功");
}
dialog.visible = false;
getList();
}
});
}
/** 删除按钮操作 */
function handleDelete(row) {
const vehicleIds = row.id || ids.value;
proxy.$modal.confirm('是否确认删除车辆编号为"' + vehicleIds + '"的数据项?').then(function() {
return proxy.$modal.msgSuccess("删除成功");
}).then(() => {
getList();
});
}
/** 导入按钮操作 */
function handleImport() {
importDialog.visible = true;
upload.title = "车辆导入";
}
/** 文件上传中处理 */
function handleFileUploadProgress(event, file, fileList) {
upload.isUploading = true;
}
/** 文件上传成功处理 */
function handleFileSuccess(response, file, fileList) {
upload.isUploading = false;
proxy.$refs["uploadRef"].clearFiles();
proxy.$alert("<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" + response.msg + "</div>", "导入结果", { dangerouslyUseHTMLString: true });
importDialog.visible = false;
getList();
}
/** 提交上传文件 */
function submitFileForm() {
proxy.$refs["uploadRef"].submit();
} }
/** 导出按钮操作 */ /** 导出按钮操作 */
function handleExport() { function handleExport() {
proxy.$modal.msgSuccess("导出功能已模拟"); proxy.download("car/park/export", {
...queryParams.value,
pageNum: undefined,
pageSize: undefined
}, `车辆数据_${new Date().getTime()}.xlsx`);
}
/** 图片上传成功处理 */
function handleImageSuccess(response, file) {
form.value.image = response.url;
}
/** 图片上传前处理 */
function beforeImageUpload(file) {
const isJPG = file.type === 'image/jpeg' || file.type === 'image/png';
const isLt2M = file.size / 1024 / 1024 < 2;
if (!isJPG) {
proxy.$message.error('上传图片只能是 JPG/PNG 格式!');
}
if (!isLt2M) {
proxy.$message.error('上传图片大小不能超过 2MB!');
}
return isJPG && isLt2M;
}
/** 表单重置 */
function reset() {
form.value = {
id: undefined,
carNo: '',
type: '',
brand: '',
organization: '',
contact: '',
phonenumber: '',
image: ''
};
proxy.resetForm("vehicleFormRef");
}
/** 下载模板操作 */
function importTemplate() {
proxy.download('car/park/importTemplate', {}, `车辆数据模板_${new Date().getTime()}.xlsx`);
}
/** 移除图片操作 */
function handleRemoveImage() {
form.value.image = '';
} }
onMounted(() => { onMounted(() => {
getList(); getList();
//
// form.value.image = 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png';
// dialog.visible = true;
// dialog.title = "";
}); });
</script> </script>
@ -377,23 +659,22 @@ onMounted(() => {
} }
// //
::v-deep(.el-input__wrapper:hover) { :deep(.el-input__wrapper:hover) {
box-shadow: none !important; box-shadow: none !important;
} }
::v-deep(.el-select) { :deep(.el-select) {
border:none !important; border:none !important;
width:160px !important; width:160px !important;
// el-selectborder
--el-border-color-hover: #343744!important; --el-border-color-hover: #343744!important;
--el-border-color: #343744!important; --el-border-color: #343744!important;
} }
::v-deep(.el-select__wrapper.is-focused) { :deep(.el-select__wrapper.is-focused) {
box-shadow: none !important; box-shadow: none !important;
} }
::v-deep(.el-select__wrapper) { :deep(.el-select__wrapper) {
background: #343744 !important; background: #343744 !important;
color: #96A0B5 !important; color: #96A0B5 !important;
border-radius: 8px !important; border-radius: 8px !important;
@ -401,10 +682,9 @@ onMounted(() => {
width: 160px; width: 160px;
font-size: 15px; font-size: 15px;
border: none !important; border: none !important;
border:none !important;
} }
::v-deep(.el-input__wrapper) { :deep(.el-input__wrapper) {
background: #343744 !important; background: #343744 !important;
color: #96A0B5 !important; color: #96A0B5 !important;
border-radius: 8px !important; border-radius: 8px !important;
@ -413,9 +693,10 @@ onMounted(() => {
box-shadow: none !important; box-shadow: none !important;
} }
::v-deep(.el-select__placeholder) { :deep(.el-select__placeholder) {
color: #96A0B5 !important; color: #96A0B5 !important;
} }
.custom-table { .custom-table {
background-color: #292c38 !important; background-color: #292c38 !important;
color: #ffffff; color: #ffffff;
@ -423,7 +704,6 @@ onMounted(() => {
border-radius: 4px; border-radius: 4px;
overflow: hidden; overflow: hidden;
//
:deep(.el-loading-mask) { :deep(.el-loading-mask) {
background-color: rgba(41, 44, 56, 0.7) !important; background-color: rgba(41, 44, 56, 0.7) !important;
} }
@ -436,7 +716,6 @@ onMounted(() => {
color: #96A0B5 !important; color: #96A0B5 !important;
} }
//
:deep(.el-table) { :deep(.el-table) {
background-color: #292c38 !important; background-color: #292c38 !important;
} }
@ -445,21 +724,8 @@ onMounted(() => {
background-color: #292c38 !important; background-color: #292c38 !important;
} }
.avatar-box{
width: 50px;
height: 50px;
border-radius: 6px;
img{
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 6px;
}
}
:deep(.el-table__inner-wrapper::before) { :deep(.el-table__inner-wrapper::before) {
display: none; /* 隐藏表格顶部的边框线 */ display: none;
} }
:deep(.el-table__header) { :deep(.el-table__header) {
@ -473,7 +739,7 @@ onMounted(() => {
background-color: #343744 !important; background-color: #343744 !important;
color: #96A0B5!important; color: #96A0B5!important;
font-weight: normal; font-weight: normal;
border-bottom: none; /* 移除th底部边线 */ border-bottom: none;
} }
tr th.el-table__cell:first-child { tr th.el-table__cell:first-child {
@ -503,7 +769,6 @@ onMounted(() => {
} }
:deep(.el-table__body-wrapper) { :deep(.el-table__body-wrapper) {
td { td {
height: 68px!important; height: 68px!important;
background-color: #292c38; background-color: #292c38;
@ -514,7 +779,6 @@ onMounted(() => {
:deep(.el-table__body tr:hover > td) { :deep(.el-table__body tr:hover > td) {
background: #2B3B5A!important; background: #2B3B5A!important;
} }
:deep(.el-button--link) { :deep(.el-button--link) {
@ -552,7 +816,7 @@ onMounted(() => {
} }
} }
/* 全局表格样式覆盖,防止闪白 */ /* 全局表格样式覆盖 */
:deep(.el-table) { :deep(.el-table) {
--el-table-bg-color: #292c38 !important; --el-table-bg-color: #292c38 !important;
--el-table-tr-bg-color: #292c38 !important; --el-table-tr-bg-color: #292c38 !important;
@ -565,7 +829,100 @@ onMounted(() => {
} }
} }
// //
.vehicle-dialog, .import-dialog {
:deep(.el-dialog) {
background-color: #343744;
border-radius: 8px;
}
:deep(.el-dialog__header) {
background-color: #343744;
color: #ffffff;
border-bottom: 1px solid #4C4F5F;
}
:deep(.el-dialog__body) {
background-color: #343744;
color: #ffffff;
}
:deep(.el-dialog__footer) {
background-color: #343744;
border-top: 1px solid #4C4F5F;
}
:deep(.el-form-item__label) {
color: #96A0B5;
}
:deep(.el-input__wrapper) {
background: #292C38 !important;
border: 1px solid #4C4F5F !important;
color: #ffffff !important;
}
:deep(.el-select__wrapper) {
background: #292C38 !important;
border: 1px solid #4C4F5F !important;
color: #ffffff !important;
}
:deep(.el-input__inner) {
color: #ffffff !important;
}
:deep(.el-select__placeholder) {
color: #96A0B5 !important;
}
}
//
.avatar-uploader {
:deep(.el-upload) {
border: 1px dashed #4C4F5F;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: var(--el-transition-duration-fast);
&:hover {
border-color: #347AE2;
}
}
:deep(.el-upload-dragger) {
background-color: #292C38;
border: 1px dashed #4C4F5F;
border-radius: 6px;
box-sizing: border-box;
width: 90px;
height: 90px;
text-align: center;
&:hover {
border-color: #347AE2;
}
}
}
.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 90px;
height: 90px;
text-align: center;
line-height: 90px;
}
.avatar {
width: 90px;
height: 90px;
display: block;
}
//
@media (max-width: 1200px) { @media (max-width: 1200px) {
.search-area { .search-area {
flex-wrap: wrap; flex-wrap: wrap;
@ -610,5 +967,199 @@ onMounted(() => {
} }
} }
} }
//
.import-dialog {
:deep(.el-dialog__header) {
margin-right: 0;
padding: 20px;
border-bottom: 1px solid #4C4F5F;
}
:deep(.el-dialog__body) {
padding: 20px;
}
:deep(.el-dialog__footer) {
padding: 20px;
border-top: 1px solid #4C4F5F;
}
.import-title {
font-size: 14px;
color: #96A0B5;
margin-bottom: 20px;
}
.import-upload {
:deep(.el-upload-dragger) {
width: 100%;
height: 200px;
background-color: #292C38;
border: 1px dashed #4C4F5F;
border-radius: 8px;
display: flex;
justify-content: center;
align-items: center;
&:hover {
border-color: #347AE2;
}
}
.upload-content {
display: flex;
flex-direction: column;
align-items: center;
color: #96A0B5;
.upload-icon {
font-size: 40px;
color: #96A0B5;
margin-bottom: 10px;
}
.upload-text {
font-size: 14px;
margin-bottom: 10px;
}
.upload-link {
display: flex;
align-items: center;
color: #347AE2;
cursor: pointer;
.upload-icon-small {
font-size: 16px;
margin-right: 4px;
}
.click-upload {
font-size: 14px;
}
}
}
}
.import-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 20px;
.footer-left {
:deep(.el-checkbox__label) {
color: #96A0B5;
}
}
.footer-right {
display: flex;
align-items: center;
gap: 10px;
.file-format {
color: #96A0B5;
font-size: 12px;
}
.download-link {
font-size: 12px;
}
}
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
}
/* 头像上传样式 */
.avatar-uploader-box {
position: relative;
display: flex;
justify-content: flex-start;
margin-top: 8px;
}
.avatar-uploader {
text-align: center;
}
.avatar-preview {
position: relative;
width: 90px;
height: 90px;
border-radius: 0;
overflow: visible;
.avatar {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 0;
border: 1px solid #343744;
}
.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: 90px;
height: 90px;
border: 1px dashed #4C4F5F;
border-radius: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
cursor: pointer;
transition: all 0.3s;
position: relative;
background-color: transparent;
overflow: visible;
&:hover {
border-color: #409EFF;
}
.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
}
}
.avatar-count {
position: absolute;
top: -5px;
right: -25px;
font-size: 14px;
color: #ffffff;
z-index: 20;
background: none;
padding: 0;
}
</style> </style>

View File

@ -1,483 +1,383 @@
<template> <template>
<div class="app-container"> <div class="app-container">
<div class="type-container"> <div class="type-container">
<!-- 搜索区域 --> <!-- Tab栏+右侧按钮 -->
<div class="search-area"> <el-tabs
<el-input v-model="editableTabsValue"
v-model="queryParams.typeName"
placeholder="请输入类型名称查询" class="demo-tabs"
clearable editable
prefix-icon="Search" @edit="handleTabsEdit"
class="search-input" >
@keyup.enter="handleQuery" <template #add-icon>
/> <el-icon><Select /></el-icon>
</template>
<div class="search-buttons"> <el-tab-pane
<el-button type="primary" class="search-btn" @click="handleQuery">搜索</el-button> v-for="item in editableTabs"
<el-button class="reset-btn" @click="resetQuery">重置</el-button> :key="item.name"
:label="item.label"
:name="item.name"
>
{{ item.content }}
</el-tab-pane>
</el-tabs>
<!-- 搜索栏和操作按钮 -->
<div class="search-action-bar">
<div class="search-area">
<el-input
v-model="query"
placeholder="请输入关键字查询"
class="search-input"
clearable
/>
<el-button type="primary" class="search-btn" @click="handleSearch"
>搜索</el-button
>
<el-button class="reset-btn" @click="handleReset">重置</el-button>
</div> </div>
<div class="action-buttons"> <div class="action-buttons">
<el-button type="primary" class="search-btn" @click="handleAdd">新增</el-button> <el-button type="primary" class="add-btn" @click="handleAdd"
<el-button class="export-btn" type="info" :disabled="multiple" @click="handleDelete">删除</el-button> >新增</el-button
>
<el-button
class="delete-btn"
@click="handleDelete"
:disabled="!selected.length"
>删除</el-button
>
</div> </div>
</div> </div>
<!-- 表格 -->
<!-- 类型表格 -->
<el-table <el-table
v-loading="loading" :data="tableData"
:data="typeList"
@selection-change="handleSelectionChange" @selection-change="handleSelectionChange"
style="width: 100%"
class="custom-table" class="custom-table"
:header-cell-style="{ backgroundColor: '#343744', color: '#fff' }" :header-cell-style="{ backgroundColor: '#343744', color: '#fff' }"
:bg-color="'#292C38'"
> >
<el-table-column type="selection" width="50" align="left" /> <el-table-column type="selection" width="50" />
<el-table-column label="序号" width="60" align="left"> <el-table-column label="操作" width="120">
<template #default="scope"> <template #default="scope">
{{ scope.$index + 1 }} <el-button link text @click="handleEdit(scope.row)">编辑</el-button>
</template> <el-button
</el-table-column> link
<el-table-column label="操作" width="120" align="left"> text
<template #default="scope"> type="danger"
<el-button link text @click="handleUpdate(scope.row)">编辑</el-button> @click="handleDelete([scope.row])"
<el-button link text type="primary" @click="handleDelete(scope.row)">删除</el-button> >删除</el-button
</template> >
</el-table-column>
<el-table-column label="类型名称" prop="typeName" align="left" />
<el-table-column label="类型编码" prop="typeCode" align="left" />
<el-table-column label="描述" prop="description" align="left" />
<el-table-column label="创建人" prop="creator" align="left" />
<el-table-column label="创建时间" align="left" prop="createTime" width="160">
<template #default="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="二级类型" prop="type" />
<el-table-column label="创建人" prop="creator" />
<el-table-column label="创建时间" prop="createTime" />
</el-table> </el-table>
<!-- 分页 -->
<!-- 分页组件 -->
<pagination <pagination
v-show="total > 0" v-show="total > 0"
:total="total" :total="total"
v-model:page="queryParams.pageNum" v-model:page="pageNum"
v-model:limit="queryParams.pageSize" v-model:limit="pageSize"
@pagination="getList" @pagination="handleTabsEdit"
/> />
</div> </div>
<!-- 添加或修改类型对话框 -->
<el-dialog :title="title" v-model="open" width="500px" append-to-body>
<el-form :model="form" :rules="rules" ref="typeRef" label-width="100px">
<el-row>
<el-col :span="24">
<el-form-item label="类型名称" prop="typeName">
<el-input
v-model="form.typeName"
placeholder="请输入类型名称"
/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item label="类型编码" prop="typeCode">
<el-input
v-model="form.typeCode"
placeholder="请输入类型编码"
/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item label="描述" prop="description">
<el-input
v-model="form.description"
type="textarea"
:rows="3"
placeholder="请输入描述信息"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button class="export-btn" @click="cancel"> </el-button>
<el-button class="search-btn" type="primary" @click="submitForm"> </el-button>
</div>
</template>
</el-dialog>
</div> </div>
</template> </template>
<script setup name="CarType"> <script setup lang="ts">
import { getCurrentInstance, ref } from 'vue'; import { ref, watch } from "vue";
import { parseTime } from "@/utils/ruoyi"; import { Select } from "@element-plus/icons-vue";
let tabIndex = 2;
const editableTabsValue = ref("2");
const editableTabs = ref([
{ label: "无人车", name: "无人车",content:'' },
{ label: "特勤车", name: "特勤车",content:''},
{ label: "普通车", name: "普通车" ,content:''},
]);
const { proxy } = getCurrentInstance(); const activeTab = ref("无人车");
const query = ref("");
const typeList = ref([]); const pageNum = ref(1);
const open = ref(false); const pageSize = ref(10);
const loading = ref(true);
const ids = ref([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0); const total = ref(0);
const title = ref(""); const selected = ref([]);
// const mockData = ref([
const mockTypeData = [
{ {
id: 1, tab: "无人车",
typeName: '无人车', list: [
typeCode: 'WRC', {
description: '无人驾驶运输车辆', id: 1,
creator: '系统管理员', type: "驱动车",
createTime: '2024-04-17 10:48:30' creator: "系统管理员",
createTime: "2024-08-17 15:48:30",
},
{
id: 2,
type: "牵引车",
creator: "系统管理员",
createTime: "2024-08-17 15:48:30",
},
],
}, },
{ {
id: 2, tab: "特勤车",
typeName: '客车', list: [
typeCode: 'KC', {
description: '载客运输车辆', id: 3,
creator: '系统管理员', type: "消防车",
createTime: '2024-04-17 10:48:30' creator: "系统管理员",
createTime: "2024-08-17 15:48:30",
},
],
}, },
{ {
id: 3, tab: "普通车",
typeName: '特种车', list: [
typeCode: 'TZC', {
description: '特种作业车辆', id: 4,
creator: '系统管理员', type: "大巴车",
createTime: '2024-04-17 10:48:30' creator: "系统管理员",
} createTime: "2024-08-17 15:48:30",
]; },
],
},
]);
const queryParams = ref({ const tableData = ref([]);
pageNum: 1, const handleTabsEdit = (targetName, action) => {
pageSize: 10, if (action === "add") {
typeName: undefined const newTabName = `${++tabIndex}`;
}); editableTabs.value.push({
label: "新标签页",
const form = ref({}); name: newTabName,
content:''
const rules = ref({ });
typeName: [ editableTabsValue.value = newTabName;
{ required: true, message: "类型名称不能为空", trigger: "blur" } } else if (action === "remove") {
], const tabs = editableTabs.value;
typeCode: [ let activeName = editableTabsValue.value;
{ required: true, message: "类型编码不能为空", trigger: "blur" } if (activeName === targetName) {
] tabs.forEach((tab, index) => {
}); if (tab.name === targetName) {
const nextTab = tabs[index + 1] || tabs[index - 1];
/** 查询类型列表 */ if (nextTab) {
function getList() { activeName = nextTab.name;
loading.value = true; }
// 使
setTimeout(() => {
loading.value = false;
typeList.value = mockTypeData;
total.value = mockTypeData.length;
}, 300);
}
/** 搜索按钮操作 */
function handleQuery() {
queryParams.value.pageNum = 1;
getList();
}
/** 重置按钮操作 */
function resetQuery() {
proxy.resetForm("queryRef");
queryParams.value.pageNum = 1;
handleQuery();
}
/** 选择条数 */
function handleSelectionChange(selection) {
ids.value = selection.map(item => item.id);
single.value = selection.length != 1;
multiple.value = !selection.length;
}
/** 删除按钮操作 */
function handleDelete(row) {
const typeIds = row.id || ids.value;
proxy.$modal.confirm('确定删除此类型吗?', '删除').then(function() {
//
getList();
proxy.$modal.msgSuccess("删除成功");
}).catch(() => {});
}
/** 新增按钮操作 */
function handleAdd() {
reset();
title.value = "添加车辆类型";
open.value = true;
}
/** 修改按钮操作 */
function handleUpdate(row) {
reset();
const typeId = row.id || ids.value[0];
// 使
const typeInfo = mockTypeData.find(d => d.id === typeId);
if (typeInfo) {
form.value = {...typeInfo};
open.value = true;
title.value = "修改车辆类型";
}
}
/** 重置表单 */
function reset() {
form.value = {
id: undefined,
typeName: undefined,
typeCode: undefined,
description: undefined
};
proxy.resetForm("typeRef");
}
/** 提交表单 */
function submitForm() {
proxy.$refs["typeRef"].validate(valid => {
if (valid) {
if (form.value.id != undefined) {
//
const index = mockTypeData.findIndex(item => item.id === form.value.id);
if (index !== -1) {
mockTypeData[index] = {...form.value};
} }
proxy.$modal.msgSuccess("修改成功"); });
open.value = false;
getList();
} else {
//
form.value.id = mockTypeData.length + 1;
form.value.createTime = new Date().toISOString().replace('T', ' ').substring(0, 19);
form.value.creator = '系统管理员';
mockTypeData.push({...form.value});
proxy.$modal.msgSuccess("新增成功");
open.value = false;
getList();
}
} }
});
}
/** 取消按钮 */ editableTabsValue.value = activeName;
function cancel() { editableTabs.value = tabs.filter((tab) => tab.name !== targetName);
open.value = false; }
reset(); };
} //
const handleSearch=()=>{
onMounted(() => { }
getList(); //
}); const handleReset=()=>{
}
//
const handleAdd=()=>{
}
//
const handleDelete=(rows)=>{
}
//
const handleSelectionChange =()=>{
}
//
const handleEdit =(rows)=>{
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.app-container { .app-container {
padding: 20px; padding: 20px;
background-color: #292C38; background-color: #292c38;
color: #ffffff; color: #ffffff;
min-height: 100%; min-height: 100%;
border-radius: 10px; border-radius: 10px;
} }
.type-container { .type-container {
background-color: #292c38; background-color: #292c38;
border-radius: 4px; border-radius: 4px;
} }
.demo-tabs {
.search-area { margin-bottom: 20px;
:deep(.el-tabs__header) {
margin-bottom: 0;
}
:deep(.el-tabs__item){
color:#fff!important;
}
}
.add-tab-btn {
width: 20px;
height: 20px;
margin-left: 8px;
cursor: pointer;
vertical-align: middle;
transition: filter 0.2s;
filter: brightness(1);
&:hover {
filter: brightness(1.3);
}
}
.search-action-bar {
display: flex; display: flex;
align-items: center; align-items: center;
margin-bottom: 20px; margin-bottom: 20px;
.search-area {
.search-input {
width: 250px;
margin-right: 10px;
}
.search-buttons {
margin-right: 20px;
display: flex; display: flex;
gap: 10px; gap: 10px;
.search-input {
width: 250px;
}
.search-btn,
.reset-btn {
border-radius: 8px;
font-size: 15px;
height: 36px;
min-width: 80px;
}
.search-btn {
background: #347ae2;
color: #fff;
border: none;
}
.reset-btn {
background: #343744;
color: #96a0b5;
border: none;
}
} }
.action-buttons { .action-buttons {
margin-left: auto; margin-left: auto;
display: flex; display: flex;
gap: 10px; gap: 10px;
.add-btn,
.delete-btn {
border-radius: 8px;
font-size: 15px;
height: 36px;
min-width: 80px;
}
.add-btn {
background: #347ae2;
color: #fff;
border: none;
}
.delete-btn {
background: #343744;
color: #96a0b5;
border: none;
}
} }
} }
//
::v-deep(.el-input__wrapper:hover) {
box-shadow: none !important;
}
::v-deep(.el-input__wrapper) {
background: #343744 !important;
color: #96A0B5 !important;
border-radius: 8px !important;
height: 36px !important;
border: none !important;
box-shadow: none !important;
}
.custom-table { .custom-table {
background-color: #292c38 !important; background-color: #292c38 !important;
color: #ffffff; color: #ffffff;
margin-bottom: 20px; margin-bottom: 20px;
border-radius: 4px; border-radius: 4px;
overflow: hidden; overflow: hidden;
//
:deep(.el-loading-mask) { :deep(.el-loading-mask) {
background-color: rgba(41, 44, 56, 0.7) !important; background-color: rgba(41, 44, 56, 0.7) !important;
} }
:deep(.el-table__empty-block) { :deep(.el-table__empty-block) {
background-color: #292c38 !important; background-color: #292c38 !important;
} }
:deep(.el-table__empty-text) { :deep(.el-table__empty-text) {
color: #96A0B5 !important; color: #96a0b5 !important;
} }
//
:deep(.el-table) { :deep(.el-table) {
background-color: #292c38 !important; background-color: #292c38 !important;
} }
:deep(.el-table__body) { :deep(.el-table__body) {
background-color: #292c38 !important; background-color: #292c38 !important;
} }
:deep(.el-table__inner-wrapper::before) { :deep(.el-table__inner-wrapper::before) {
display: none; /* 隐藏表格顶部的边框线 */ display: none;
} }
:deep(.el-table__header) { :deep(.el-table__header) {
border-top-left-radius: 4px; border-top-left-radius: 4px;
border-top-right-radius: 4px; border-top-right-radius: 4px;
overflow: hidden; overflow: hidden;
} }
:deep(.el-table__header-wrapper) { :deep(.el-table__header-wrapper) {
th { th {
background-color: #343744 !important; background-color: #343744 !important;
color: #96A0B5 !important; color: #96a0b5 !important;
font-weight: normal; font-weight: normal;
border-bottom: none; /* 移除th底部边线 */ border-bottom: none;
} }
tr th.el-table__cell:first-child { tr th.el-table__cell:first-child {
border-top-left-radius: 6px; border-top-left-radius: 6px;
.cell { .cell {
border-top-left-radius: 6px; border-top-left-radius: 6px;
} }
} }
tr th.el-table__cell:last-child { tr th.el-table__cell:last-child {
border-top-right-radius: 4px; border-top-right-radius: 4px;
.cell { .cell {
border-top-right-radius: 4px; border-top-right-radius: 4px;
} }
} }
.el-table__header th.el-table__cell { .el-table__header th.el-table__cell {
background-color: #343744 !important; background-color: #343744 !important;
color: #96A0B5; color: #96a0b5;
} }
tr { tr {
background-color: #343744 !important; background-color: #343744 !important;
} }
} }
:deep(.el-table__body-wrapper) { :deep(.el-table__body-wrapper) {
td { td {
height: 68px!important; height: 68px !important;
background-color: #292c38; background-color: #292c38;
color: #ffffff; color: #ffffff;
border-bottom: 1px solid #4C4F5F; border-bottom: 1px solid #4c4f5f;
} }
} }
:deep(.el-table__body tr:hover > td) { :deep(.el-table__body tr:hover > td) {
background: #2B3B5A!important; background: #2b3b5a !important;
} }
:deep(.el-button--link) { :deep(.el-button--link) {
border: none; border: none;
background: transparent; background: transparent;
color: #347AE2; color: #347ae2;
padding: 0; padding: 0;
margin: 0 5px; margin: 0 5px;
} }
:deep(.el-button.is-text) { :deep(.el-button.is-text) {
background-color: transparent !important; background-color: transparent !important;
border-color: transparent; border-color: transparent;
color: #347AE2; color: #347ae2;
&:hover,
&:hover, &:focus, &:active { &:focus,
&:active {
background-color: transparent !important; background-color: transparent !important;
color: #347AE2; color: #347ae2;
} }
} }
:deep(.el-button.el-button--primary.is-text) { :deep(.el-button.el-button--primary.is-text) {
background-color: transparent !important; background-color: transparent !important;
color: #347AE2; color: #347ae2;
&:hover { &:hover {
background-color: transparent !important; background-color: transparent !important;
color: #347AE2; color: #347ae2;
} }
} }
:deep(.el-table__body tr:hover .el-button--link.is-text) { :deep(.el-table__body tr:hover .el-button--link.is-text) {
background-color: transparent !important; background-color: transparent !important;
color: #347AE2; color: #347ae2;
}
}
//
:deep(.el-dialog) {
.el-form-item__label {
color: #96A0B5 !important;
}
.el-input__wrapper {
background-color: #292c38 !important;
box-shadow: 0 0 0 1px #565C70 inset !important;
.el-input__inner {
color: #ffffff !important;
}
}
.el-textarea__inner {
background-color: #292c38 !important;
border-color: #565C70 !important;
color: #ffffff !important;
&:focus {
box-shadow: 0 0 0 1px #409EFF inset !important;
}
} }
} }
</style> </style>

View File

@ -154,22 +154,31 @@ getCookie();
justify-content: center; justify-content: center;
align-items: center; align-items: center;
height: 100%; height: 100%;
background-image: url("../assets/images/login-background.jpg"); background-image: url("../assets/images/login-background.png");
background-size: cover; background-size: 100% 100%;
} }
.title { .title {
margin: 0px auto 30px auto; margin: 0px auto 30px auto;
text-align: center; text-align: center;
color: #707070; color: #ffffff;
font-size: 20px;
font-weight: normal;
} }
.login-form { .login-form {
border-radius: 6px; border-radius: 8px;
background: #ffffff; background: rgba(41, 44, 56, 0.8);
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
width: 400px; width: 400px;
padding: 25px 25px 5px 25px; padding: 25px 25px 5px 25px;
.el-input { .el-input {
height: 40px; height: 40px;
:deep(.el-input__inner) {
color: #ffffff;
height: 40px;
}
input { input {
height: 40px; height: 40px;
} }

View File

@ -1,16 +1,29 @@
<template> <template>
<div class="platform-overview platform-no-padding"> <div class="platform-overview platform-no-padding">
<OpenLayersMap /> <OpenLayersMap ref="mapRef" />
<CarAlarm /> <CarAlarm />
<!-- 右侧展开/收起按钮 --> <!-- 右侧展开/收起按钮 -->
<img <img
class="eventlist-toggle-btn" class="eventlist-toggle-btn"
:class="{ 'btn-opened': showEventList }"
:src="showEventList ? btn_left : btn_right" :src="showEventList ? btn_left : btn_right"
:alt="showEventList ? '收起车辆列表' : '展开车辆列表'" :alt="showEventList ? '收起车辆列表' : '展开车辆列表'"
@click="toggleEventList" @click="toggleEventList"
/> />
<!-- 车辆列表 --> <!-- 车辆列表 -->
<Eventlist v-if="showEventList" /> <Eventlist v-if="showEventList" />
<!-- 绘制工具栏 -->
<!-- <div class="draw-toolbar">
<div class="toolbar-title">绘制工具</div>
<div class="toolbar-buttons">
<button @click="startDrawLine" class="toolbar-button">绘制路线</button>
<button @click="startDrawPolygon" class="toolbar-button">绘制点线面</button>
<button @click="clearDraw" class="toolbar-button clear">清除</button>
<button @click="exportRouteData" class="toolbar-button export">导出数据</button>
<button @click="importRouteData" class="toolbar-button import">导入数据</button>
</div>
</div>-->
</div> </div>
</template> </template>
@ -22,12 +35,54 @@ import Eventlist from '../../components/map/info/eventlist.vue';
import btn_left from '../../assets/images/btn_left.png'; import btn_left from '../../assets/images/btn_left.png';
import btn_right from '../../assets/images/btn_right.png'; import btn_right from '../../assets/images/btn_right.png';
const showEventList = ref(false); const showEventList = ref(false); //
const mapRef = ref(null);
function toggleEventList() { function toggleEventList() {
showEventList.value = !showEventList.value; showEventList.value = !showEventList.value;
} }
function getRouteDrawControlExpose() {
// OpenLayersMaprefRouteDrawControlref
return mapRef.value?.$refs?.routeDrawControlRef;
}
function startDrawLine() {
const routeDraw = getRouteDrawControlExpose();
if (routeDraw && routeDraw.startDrawLine) {
routeDraw.startDrawLine();
}
}
function startDrawPolygon() {
const routeDraw = getRouteDrawControlExpose();
if (routeDraw && routeDraw.startDrawPolygon) {
routeDraw.startDrawPolygon();
}
}
function clearDraw() {
const routeDraw = getRouteDrawControlExpose();
if (routeDraw && routeDraw.clearDraw) {
routeDraw.clearDraw();
}
}
function exportRouteData() {
const routeDraw = getRouteDrawControlExpose();
if (routeDraw && routeDraw.exportRouteData) {
routeDraw.exportRouteData();
}
}
function importRouteData() {
const routeDraw = getRouteDrawControlExpose();
if (routeDraw && routeDraw.showImportDialog) {
// RouteDrawControl
routeDraw.showImportDialog = true;
}
}
onMounted(() => { onMounted(() => {
document.querySelector('.app-main')?.classList.add('platform-no-padding'); document.querySelector('.app-main')?.classList.add('platform-no-padding');
}); });
@ -57,13 +112,90 @@ onUnmounted(() => {
height: 80px; height: 80px;
transform: translateY(-50%); transform: translateY(-50%);
cursor: pointer; cursor: pointer;
transition: right 0.3s ease;
}
/* 按钮打开状态 - 移到左侧 */
.eventlist-toggle-btn.btn-opened {
right: 405px;
}
/* 绘制工具栏 */
.draw-toolbar {
position: absolute;
top: 20px;
right: 20px;
background-color: rgba(255, 255, 255, 0.9);
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
z-index: 2000;
width: 200px;
overflow: hidden;
}
.toolbar-title {
padding: 10px;
background-color: #0096ff;
color: white;
font-weight: bold;
text-align: center;
}
.toolbar-buttons {
padding: 10px;
display: flex;
flex-direction: column;
gap: 8px;
}
.toolbar-button {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #f5f5f5;
cursor: pointer;
transition: all 0.2s;
}
.toolbar-button:hover {
background-color: #e0e0e0;
}
.toolbar-button.clear {
background-color: #f44336;
color: white;
border-color: #d32f2f;
}
.toolbar-button.clear:hover {
background-color: #d32f2f;
}
.toolbar-button.export {
background-color: #4caf50;
color: white;
border-color: #388e3c;
}
.toolbar-button.export:hover {
background-color: #388e3c;
}
.toolbar-button.import {
background-color: #ff9800;
color: white;
border-color: #f57c00;
}
.toolbar-button.import:hover {
background-color: #f57c00;
} }
/* 只影响本页精准去除AppMain的左右padding */ /* 只影响本页精准去除AppMain的左右padding */
:deep(.platform-no-padding) .app-main { // :deep(.platform-no-padding) .app-main {
padding-left: 0 !important; // padding-left: 0 !important;
padding-right: 0 !important; // padding-right: 0 !important;
} // }
</style> </style>
<style> <style>

View File

@ -716,11 +716,11 @@ onMounted(() => {
} }
// //
::v-deep(.el-input__wrapper:hover) { :deep(.el-input__wrapper:hover) {
box-shadow: none !important; box-shadow: none !important;
} }
::v-deep(.el-select) { :deep(.el-select) {
border:none !important; border:none !important;
width:140px !important; width:140px !important;
// el-selectborder // el-selectborder
@ -728,11 +728,11 @@ onMounted(() => {
--el-border-color: #343744!important; --el-border-color: #343744!important;
} }
::v-deep(.el-select__wrapper.is-focused) { :deep(.el-select__wrapper.is-focused) {
box-shadow: none !important; box-shadow: none !important;
} }
::v-deep(.el-select__wrapper) { :deep(.el-select__wrapper) {
background: #343744 !important; background: #343744 !important;
color: #96A0B5 !important; color: #96A0B5 !important;
border-radius: 8px !important; border-radius: 8px !important;
@ -743,7 +743,7 @@ onMounted(() => {
border:none !important; border:none !important;
} }
::v-deep(.el-input__wrapper) { :deep(.el-input__wrapper) {
background: #343744 !important; background: #343744 !important;
color: #96A0B5 !important; color: #96A0B5 !important;
border-radius: 8px !important; border-radius: 8px !important;
@ -752,7 +752,7 @@ onMounted(() => {
box-shadow: none !important; box-shadow: none !important;
} }
::v-deep(.el-select__placeholder) { :deep(.el-select__placeholder) {
color: #96A0B5 !important; color: #96A0B5 !important;
} }
.custom-table { .custom-table {

View File

@ -269,11 +269,11 @@ getList();
} }
// //
::v-deep(.el-input__wrapper:hover) { :deep(.el-input__wrapper:hover) {
box-shadow: none !important; box-shadow: none !important;
} }
::v-deep(.el-select) { :deep(.el-select) {
border: none !important; border: none !important;
width: 140px !important; width: 140px !important;
// el-selectborder // el-selectborder
@ -281,11 +281,11 @@ getList();
--el-border-color: #343744 !important; --el-border-color: #343744 !important;
} }
::v-deep(.el-select__wrapper.is-focused) { :deep(.el-select__wrapper.is-focused) {
box-shadow: none !important; box-shadow: none !important;
} }
::v-deep(.el-select__wrapper) { :deep(.el-select__wrapper) {
background: #343744 !important; background: #343744 !important;
color: #96A0B5 !important; color: #96A0B5 !important;
border-radius: 8px !important; border-radius: 8px !important;
@ -295,7 +295,7 @@ getList();
border: none !important; border: none !important;
} }
::v-deep(.el-input__wrapper) { :deep(.el-input__wrapper) {
background: #343744 !important; background: #343744 !important;
color: #96A0B5 !important; color: #96A0B5 !important;
border-radius: 8px !important; border-radius: 8px !important;
@ -304,7 +304,7 @@ getList();
box-shadow: none !important; box-shadow: none !important;
} }
::v-deep(.el-select__placeholder) { :deep(.el-select__placeholder) {
color: #96A0B5 !important; color: #96A0B5 !important;
} }

View File

@ -509,11 +509,11 @@ getList();
} }
// //
::v-deep(.el-input__wrapper:hover) { :deep(.el-input__wrapper:hover) {
box-shadow: none !important; box-shadow: none !important;
} }
::v-deep(.el-select) { :deep(.el-select) {
border:none !important; border:none !important;
width:140px !important; width:140px !important;
// el-selectborder // el-selectborder
@ -521,11 +521,11 @@ getList();
--el-border-color: #343744!important; --el-border-color: #343744!important;
} }
::v-deep(.el-select__wrapper.is-focused) { :deep(.el-select__wrapper.is-focused) {
box-shadow: none !important; box-shadow: none !important;
} }
::v-deep(.el-select__wrapper) { :deep(.el-select__wrapper) {
background: #343744 !important; background: #343744 !important;
color: #96A0B5 !important; color: #96A0B5 !important;
border-radius: 8px !important; border-radius: 8px !important;
@ -536,7 +536,7 @@ getList();
border:none !important; border:none !important;
} }
::v-deep(.el-input__wrapper) { :deep(.el-input__wrapper) {
background: #343744 !important; background: #343744 !important;
color: #96A0B5 !important; color: #96A0B5 !important;
border-radius: 8px !important; border-radius: 8px !important;
@ -545,7 +545,7 @@ getList();
box-shadow: none !important; box-shadow: none !important;
} }
::v-deep(.el-select__placeholder) { :deep(.el-select__placeholder) {
color: #96A0B5 !important; color: #96A0B5 !important;
} }
.custom-table { .custom-table {

View File

@ -558,11 +558,11 @@ function getPostOptions() {
} }
} }
// //
::v-deep(.el-input__wrapper:hover) { :deep(.el-input__wrapper:hover) {
box-shadow: none !important; box-shadow: none !important;
} }
::v-deep(.el-select) { :deep(.el-select) {
border:none !important; border:none !important;
width:140px !important; width:140px !important;
// el-selectborder // el-selectborder
@ -570,11 +570,11 @@ function getPostOptions() {
--el-border-color: #343744!important; --el-border-color: #343744!important;
} }
::v-deep(.el-select__wrapper.is-focused) { :deep(.el-select__wrapper.is-focused) {
box-shadow: none !important; box-shadow: none !important;
} }
::v-deep(.el-select__wrapper) { :deep(.el-select__wrapper) {
background: #343744 !important; background: #343744 !important;
color: #96A0B5 !important; color: #96A0B5 !important;
border-radius: 8px !important; border-radius: 8px !important;
@ -585,7 +585,7 @@ function getPostOptions() {
border:none !important; border:none !important;
} }
::v-deep(.el-input__wrapper) { :deep(.el-input__wrapper) {
background: #343744 !important; background: #343744 !important;
color: #96A0B5 !important; color: #96A0B5 !important;
border-radius: 8px !important; border-radius: 8px !important;
@ -594,7 +594,7 @@ function getPostOptions() {
box-shadow: none !important; box-shadow: none !important;
} }
::v-deep(.el-select__placeholder) { :deep(.el-select__placeholder) {
color: #96A0B5 !important; color: #96A0B5 !important;
} }
.custom-table { .custom-table {

View File

@ -31,7 +31,7 @@ export default defineConfig(({ mode, command }) => {
proxy: { proxy: {
// https://cn.vitejs.dev/config/#server-proxy // https://cn.vitejs.dev/config/#server-proxy
'/dev-api': { '/dev-api': {
target: 'http://localhost:8099', target: 'http://10.0.0.17:8099',
changeOrigin: true, changeOrigin: true,
rewrite: (p) => p.replace(/^\/dev-api/, '') rewrite: (p) => p.replace(/^\/dev-api/, '')
} }