地图组件更新
This commit is contained in:
parent
86999cc440
commit
dba5d7ca8b
@ -49,6 +49,7 @@
|
||||
"js-cookie": "^3.0.1",
|
||||
"jsencrypt": "^3.3.2",
|
||||
"jspdf": "^2.5.1",
|
||||
"leaflet": "^1.9.4",
|
||||
"marked": "^4.0.17",
|
||||
"minimatch": "^5.1.0",
|
||||
"monaco-editor": "^0.36.1",
|
||||
@ -78,6 +79,7 @@
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^16.2.3",
|
||||
"@commitlint/config-conventional": "^16.2.1",
|
||||
"@types/leaflet": "^1.9.17",
|
||||
"@types/node": "^16.11.7",
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"@types/path-browserify": "^1.0.0",
|
||||
|
@ -167,6 +167,14 @@ export function startSimpleNavi(params:any){
|
||||
params:params
|
||||
})
|
||||
}
|
||||
//关闭
|
||||
export function stopSimpleNavi(params:any){
|
||||
return request ({
|
||||
url:'/experimentalData/ts-files/stopSimpleNavi',
|
||||
method:'post',
|
||||
params:params
|
||||
})
|
||||
}
|
||||
//读取text文件
|
||||
export function apicontent(params:any){
|
||||
return request ({
|
||||
|
@ -1,282 +1,137 @@
|
||||
<template>
|
||||
<div ref="mapContainer" class="map-container" :class="{ 'fullscreen': isFullscreen }">
|
||||
<button class="fullscreen-btn" @click="toggleFullscreen">
|
||||
{{ isFullscreen ? '退出全屏' : '全屏' }}
|
||||
</button>
|
||||
</div>
|
||||
<!-- 地图容器 -->
|
||||
<div id="map" style="height: 800px; width: 100%"></div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch, defineEmits, onBeforeUnmount } from 'vue'
|
||||
import * as d3 from 'd3'
|
||||
import chinaData from './newJson.json'
|
||||
// ==================== 常量与类型定义 ====================
|
||||
const DEFAULT_MAP_SIZE = { width: 800, height: 600 }
|
||||
const PROJECTION_CENTER = [104, 37]
|
||||
const SCALE_FACTORS = { normal: 600, fullscreen: 1000 }
|
||||
<script setup lang="ts">
|
||||
import { onMounted, watch, nextTick, onUnmounted } from 'vue'
|
||||
import L from 'leaflet'
|
||||
import 'leaflet/dist/leaflet.css'
|
||||
|
||||
// ==================== 组件Props/Emits ====================
|
||||
const props = defineProps({
|
||||
coordinates: {
|
||||
type: Array,
|
||||
default: () => [
|
||||
[116.405285, 39.904989],
|
||||
[121.472644, 31.231706]
|
||||
],
|
||||
validator: value => value.every(coord =>
|
||||
Array.isArray(coord) && coord.length === 2 &&
|
||||
typeof coord[0] === 'number' && typeof coord[1] === 'number'
|
||||
)
|
||||
},
|
||||
qvehuan: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
// 类型定义
|
||||
interface PointProp {
|
||||
points?: Array<[number, number]>;
|
||||
}
|
||||
|
||||
// 地图配置常量
|
||||
const MAP_CONFIG = {
|
||||
center: [39.9042, 116.4074] as [number, number], // 初始中心坐标(北京)
|
||||
zoom: 4, // 初始缩放级别
|
||||
minZoom: 0, // 最小缩放级别
|
||||
maxZoom: 18, // 最大缩放级别
|
||||
zoomControl: true // 缩放控件
|
||||
}
|
||||
|
||||
// 轨迹线样式配置
|
||||
const POLYLINE_STYLE = {
|
||||
color: 'blue', // 轨迹线颜色
|
||||
weight: 3, // 线宽
|
||||
lineJoin: 'round' as const // 连接处样式
|
||||
}
|
||||
|
||||
// 响应式props定义
|
||||
const props = withDefaults(defineProps<PointProp>(), {
|
||||
points: () => []
|
||||
})
|
||||
|
||||
const emit = defineEmits(['qvehuan1'])
|
||||
// 地图实例引用
|
||||
let map: L.Map | null = null
|
||||
// 轨迹线实例
|
||||
let polyline: L.Polyline | null = null
|
||||
// 标记点数组
|
||||
let markers: L.Marker[] = []
|
||||
|
||||
// ==================== 响应式状态 ====================
|
||||
const mapContainer = ref(null)
|
||||
const isFullscreen = ref(false)
|
||||
const isRouteInitialized = ref(false)
|
||||
|
||||
// ==================== D3相关变量 ====================
|
||||
let svgInstance = null
|
||||
let mapGroup = null
|
||||
let markersGroup = null
|
||||
let zoomBehavior = null
|
||||
|
||||
// ==================== 地图核心逻辑 ====================
|
||||
const createProjection = () => {
|
||||
const { width, height } = getContainerSize()
|
||||
const projection = d3.geoMercator()
|
||||
|
||||
if (props.coordinates?.length >= 2) {
|
||||
const validCoords = props.coordinates.filter(c => isValidCoordinate(c))
|
||||
if (validCoords.length >= 2) {
|
||||
const routeFeature = {
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
type: 'LineString',
|
||||
coordinates: validCoords
|
||||
}
|
||||
}
|
||||
|
||||
projection.fitExtent(
|
||||
[[50, 50], [width - 50, height - 50]], // 增加边距
|
||||
routeFeature
|
||||
)
|
||||
return projection
|
||||
}
|
||||
}
|
||||
|
||||
return projection
|
||||
.center(PROJECTION_CENTER)
|
||||
.scale(isFullscreen.value ? SCALE_FACTORS.fullscreen : SCALE_FACTORS.normal)
|
||||
.translate([width / 2, height / 2])
|
||||
/**
|
||||
* 初始化地图实例
|
||||
*/
|
||||
function initMap() {
|
||||
map = L.map('map', MAP_CONFIG)
|
||||
addTileLayer()
|
||||
}
|
||||
|
||||
const initializeMap = () => {
|
||||
if (svgInstance) svgInstance.remove()
|
||||
|
||||
const { width, height } = getContainerSize()
|
||||
const projection = createProjection()
|
||||
const pathGenerator = d3.geoPath().projection(projection)
|
||||
|
||||
svgInstance = d3.select(mapContainer.value)
|
||||
.append('svg')
|
||||
.attr('width', width)
|
||||
.attr('height', height)
|
||||
.style('background-color', '#f0f8ff')
|
||||
|
||||
const mainGroup = svgInstance.append('g')
|
||||
mapGroup = mainGroup.append('g').attr('class', 'map-layer')
|
||||
markersGroup = mainGroup.append('g').attr('class', 'markers-layer')
|
||||
|
||||
renderChinaMap(pathGenerator)
|
||||
initializeZoomBehavior(mainGroup)
|
||||
/**
|
||||
* 添加离线瓦片图层
|
||||
*/
|
||||
function addTileLayer() {
|
||||
L.tileLayer('/tiles/{z}/{y}/{x}.jpg', {
|
||||
attribution: 'Offline Map', // 图层属性说明
|
||||
tileSize: 256, // 瓦片尺寸
|
||||
noWrap: true // 禁止重复瓦片
|
||||
}).addTo(map!)
|
||||
}
|
||||
|
||||
const renderChinaMap = (path) => {
|
||||
mapGroup.append('g')
|
||||
.selectAll('path')
|
||||
.data(chinaData.features)
|
||||
.enter()
|
||||
.append('path')
|
||||
.attr('d', path)
|
||||
.attr('fill', '#e7e7e7')
|
||||
.attr('stroke', '#fff')
|
||||
.attr('stroke-width', 0.3)
|
||||
|
||||
mapGroup.append('g')
|
||||
.selectAll('text')
|
||||
.data(chinaData.features)
|
||||
.enter()
|
||||
.append('text')
|
||||
.attr('transform', d => `translate(${path.centroid(d)})`)
|
||||
.text(d => d.properties.name || '')
|
||||
.attr('text-anchor', 'middle')
|
||||
.style('font', '12px Arial')
|
||||
.attr('fill', '#333')
|
||||
.style('pointer-events', 'none')
|
||||
/**
|
||||
* 清除所有轨迹元素
|
||||
*/
|
||||
function clearLayers() {
|
||||
// 移除轨迹线
|
||||
polyline?.remove()
|
||||
// 批量移除标记点
|
||||
markers.forEach(marker => marker.remove())
|
||||
markers = []
|
||||
}
|
||||
|
||||
// ==================== 路线相关逻辑 ====================
|
||||
const updateRoute = () => {
|
||||
if (!props.coordinates?.length) return
|
||||
/**
|
||||
* 将坐标转换为Leaflet的LatLng对象
|
||||
* @param point 原始坐标数组
|
||||
* @returns Leaflet坐标对象
|
||||
*/
|
||||
const convertToLatLng = (point: [number, number]) => L.latLng(point[0], point[1])
|
||||
|
||||
/**
|
||||
* 绘制轨迹线
|
||||
* @param latlngs 坐标点数组
|
||||
*/
|
||||
function createPolyline(latlngs: L.LatLng[]) {
|
||||
polyline = L.polyline(latlngs, POLYLINE_STYLE).addTo(map!)
|
||||
// 自动适配视图(按需启用)
|
||||
// map?.fitBounds(polyline.getBounds())
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建标记点
|
||||
* @param latlngs 坐标点数组
|
||||
* @returns 标记实例数组
|
||||
*/
|
||||
function createMarkers(latlngs: L.LatLng[]) {
|
||||
return latlngs.map(latlng =>
|
||||
L.marker(latlng).addTo(map!)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 主绘制函数
|
||||
* @param points 原始坐标数组
|
||||
*/
|
||||
function drawTrajectory(points: Array<[number, number]>) {
|
||||
if (!map) return
|
||||
|
||||
initializeMap()
|
||||
// 始终执行清理(无论是否有新坐标)
|
||||
clearLayers()
|
||||
|
||||
const projection = createProjection()
|
||||
const pathGenerator = d3.geoPath().projection(projection)
|
||||
|
||||
if (isRouteInitialized.value) {
|
||||
mapGroup.selectAll('.route-path').remove()
|
||||
markersGroup.selectAll('*').remove()
|
||||
isRouteInitialized.value = false
|
||||
emit('qvehuan1', isRouteInitialized.value)
|
||||
// 仅在有有效坐标时创建新元素
|
||||
if (points?.length > 0) {
|
||||
const latlngs = points.map(convertToLatLng)
|
||||
createPolyline(latlngs)
|
||||
markers = createMarkers(latlngs)
|
||||
}
|
||||
|
||||
renderRoutePath(pathGenerator)
|
||||
renderMarkers(projection)
|
||||
}
|
||||
|
||||
const renderRoutePath = (path) => {
|
||||
const routeData = {
|
||||
type: "LineString",
|
||||
coordinates: props.coordinates
|
||||
}
|
||||
|
||||
mapGroup.append('path')
|
||||
.datum(routeData)
|
||||
.attr('class', 'route-path')
|
||||
.attr('d', path)
|
||||
.attr('fill', 'none')
|
||||
.attr('stroke', '#ff4757')
|
||||
.attr('stroke-width', 2)
|
||||
.attr('stroke-linecap', 'round')
|
||||
}
|
||||
|
||||
|
||||
|
||||
const renderMarkers = (projection) => {
|
||||
props.coordinates.forEach((coord, index) => {
|
||||
if (!isValidCoordinate(coord)) return
|
||||
|
||||
const [x, y] = projection(coord)
|
||||
// 标记点增强显示
|
||||
markersGroup.append('circle')
|
||||
.attr('cx', x)
|
||||
.attr('cy', y)
|
||||
.attr('r', 4) // 增大半径
|
||||
.attr('fill', '#ff4757')
|
||||
.attr('stroke', 'white')
|
||||
.attr('stroke-width', 1)
|
||||
.raise() // 确保标记点在最上层
|
||||
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== 工具函数 ====================
|
||||
const getContainerSize = () => ({
|
||||
width: mapContainer.value?.clientWidth || DEFAULT_MAP_SIZE.width,
|
||||
height: mapContainer.value?.clientHeight || DEFAULT_MAP_SIZE.height
|
||||
})
|
||||
|
||||
const isValidCoordinate = (coord) =>
|
||||
Array.isArray(coord) && coord.length === 2 &&
|
||||
!isNaN(coord[0]) && !isNaN(coord[1])
|
||||
|
||||
// ==================== 交互逻辑 ====================
|
||||
const initializeZoomBehavior = (group) => {
|
||||
zoomBehavior = d3.zoom()
|
||||
.scaleExtent([0.2, 68])
|
||||
.on('zoom', ({ transform }) => {
|
||||
group.attr('transform', transform)
|
||||
})
|
||||
svgInstance.call(zoomBehavior)
|
||||
}
|
||||
|
||||
const handleWindowResize = () => {
|
||||
initializeMap()
|
||||
updateRoute()
|
||||
}
|
||||
|
||||
const toggleFullscreen = () => {
|
||||
isFullscreen.value = !isFullscreen.value
|
||||
setTimeout(() => {
|
||||
initializeMap()
|
||||
updateRoute()
|
||||
}, 100)
|
||||
}
|
||||
|
||||
// ==================== 生命周期钩子 ====================
|
||||
// 组件挂载时初始化地图
|
||||
onMounted(() => {
|
||||
window.addEventListener('resize', handleWindowResize)
|
||||
initializeMap()
|
||||
if (props.coordinates?.length) {
|
||||
updateRoute()
|
||||
}
|
||||
nextTick(() => {
|
||||
initMap()
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', handleWindowResize)
|
||||
// 组件卸载时清理地图
|
||||
onUnmounted(() => {
|
||||
map?.remove()
|
||||
map = null
|
||||
})
|
||||
|
||||
watch(() => props.qvehuan, (newVal) => {
|
||||
isRouteInitialized.value = newVal
|
||||
updateRoute()
|
||||
})
|
||||
|
||||
watch(() => props.coordinates, () => {
|
||||
updateRoute()
|
||||
}, { deep: true })
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.map-container {
|
||||
position: relative;
|
||||
background-color: #f0f8ff;
|
||||
margin: 20px;
|
||||
overflow: hidden;
|
||||
touch-action: none;
|
||||
transition: all 0.3s ease;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.fullscreen {
|
||||
position: fixed !important;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
margin: 0 !important;
|
||||
z-index: 1000;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.fullscreen-btn {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 30px;
|
||||
z-index: 1001;
|
||||
padding: 10px 20px;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.fullscreen-btn:hover {
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
</style>
|
||||
// 监听points变化自动绘制轨迹
|
||||
watch(() => props.points, (newPoints) => {
|
||||
drawTrajectory(newPoints || [])
|
||||
}, { deep: true, immediate: true })
|
||||
</script>
|
@ -218,7 +218,7 @@ function handleClose(formEl: any) {
|
||||
//删除子节点
|
||||
function delSubItem(row: any) {
|
||||
ElMessageBox.confirm(
|
||||
'您确定要删除该项目吗?',
|
||||
'您确定要删除该节点及其中的文件吗?',
|
||||
'警告',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
@ -227,6 +227,8 @@ function delSubItem(row: any) {
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
treeloading.value = true
|
||||
loading.value = true
|
||||
deleteNodesById({ id: row.id }).then((res: any) => {
|
||||
if (res.code == 0) {
|
||||
gettreedata()
|
||||
|
@ -67,7 +67,7 @@ function editproject(row: any) {
|
||||
//删除项目弹框
|
||||
function delproject(row: any) {
|
||||
ElMessageBox.confirm(
|
||||
'您确定要删除该项目吗?',
|
||||
'您确定要删除该项目及其中的节点和文件吗?',
|
||||
'警告',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
@ -76,6 +76,7 @@ function delproject(row: any) {
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
loading.value = true
|
||||
deleteSdprojectById({ id: row.id }).then((res: any) => {
|
||||
if (res.code == 0) {
|
||||
getdata()
|
||||
@ -102,7 +103,7 @@ function delprojectArr() {
|
||||
ids.push(item.id)
|
||||
})
|
||||
ElMessageBox.confirm(
|
||||
'您确定要删除这些项目吗?',
|
||||
'您确定要删除这些项目及其中的节点和文件吗?',
|
||||
'警告',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
@ -111,6 +112,7 @@ function delprojectArr() {
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
loading.value = true
|
||||
deleteSdprojectByIds({ ids: ids.join(',') }).then((res: any) => {
|
||||
if (res.code == 0) {
|
||||
ElMessage({
|
||||
|
19
web/src/views/testdata/datamanagement/index.vue
vendored
19
web/src/views/testdata/datamanagement/index.vue
vendored
@ -13,7 +13,7 @@ import { ElMessageBox, ElMessage, ElMain } from "element-plus";
|
||||
import Page from '@/components/Pagination/page.vue';
|
||||
import AudioPlayer from '@/components/file/preview/AudioPlayer.vue';
|
||||
import { batchDeleteReq } from "@/api/file-operator";
|
||||
import { tstaskList, getTsNodesTree, addTsNodes, updateTsNodes, deleteTsNodesById, tsFilesPage, addTsFiles, updateTsFiles, deleteTsFilesById, listTsFiles, deleteTsFilesByIds, compress, Decompression, compare, downloadToLocal, uploadToBackup, addTsFile, list, moveFileFolder, copyFileFolder, startSimpleNavi } from "@/api/datamanagement";
|
||||
import { tstaskList, getTsNodesTree, addTsNodes, updateTsNodes, deleteTsNodesById, tsFilesPage, addTsFiles, updateTsFiles, deleteTsFilesById, listTsFiles, deleteTsFilesByIds, compress, Decompression, compare, downloadToLocal, uploadToBackup, addTsFile, list, moveFileFolder, copyFileFolder, startSimpleNavi,stopSimpleNavi } from "@/api/datamanagement";
|
||||
import ZUpload from '@/components/file/ZUpload.vue'
|
||||
import useFileUpload from "@/components/file/file/useFileUpload";
|
||||
import useHeaderStorageList from "@/components/header/useHeaderStorageList";
|
||||
@ -1412,7 +1412,6 @@ function openMap(row: any) {
|
||||
}
|
||||
function mapClose() {
|
||||
mapTrajectory.value = false
|
||||
|
||||
closeSSE()
|
||||
// index = 0
|
||||
}
|
||||
@ -1455,6 +1454,7 @@ function frequency(row: any) {
|
||||
qvehuan.value = true
|
||||
qvehuan1.value = true
|
||||
lineData.value.length = 0
|
||||
dynamicCoordinates.value.length = 0
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -1462,11 +1462,11 @@ function frequency(row: any) {
|
||||
const eventSource = ref(null)
|
||||
let index = 0
|
||||
const dynamicCoordinates = ref([])
|
||||
let SSEclose
|
||||
// let SSEclose
|
||||
function closeSSE() {
|
||||
dynamicCoordinates.value.length = 0
|
||||
// SSEclose :any
|
||||
SSEclose = new EventSource(userStore.webApiBaseUrl + '/sse/disconnect/' + userStore.userId)
|
||||
stopSimpleNavi({ token: userStore.userId }).then((res: any) => {
|
||||
})
|
||||
eventSource.value?.close()
|
||||
}
|
||||
function getSSELink() {
|
||||
@ -1482,10 +1482,7 @@ function getSSELink() {
|
||||
const data = JSON.parse(e.data)
|
||||
console.log('SSE消息:', data)
|
||||
if (data) {
|
||||
dynamicCoordinates.value.push([data.lon, data.lat])
|
||||
// if (dynamicCoordinates.value.length > 2) {
|
||||
// dynamicCoordinates.value.shift()
|
||||
// }
|
||||
dynamicCoordinates.value.push([data.lat, data.lon])
|
||||
lineData.value.push({ x: data.UtcTime, y: data.alt })
|
||||
}
|
||||
} catch (err) {
|
||||
@ -1919,8 +1916,8 @@ function texexceltClose() {
|
||||
<div @click="tabbas = '1'" :class="tabbas == '1' ? 'mapbox_border' : 'mapbox_border1'">载体运动轨迹</div>
|
||||
<div @click="tabbas = '2'" :class="tabbas == '2' ? 'mapbox_border' : 'mapbox_border1'">高程变化动态图</div>
|
||||
</div>
|
||||
<div v-show="tabbas == '1'" style="width: 800px;height:600px;overflow: hidden;">
|
||||
<MapChart :coordinates="dynamicCoordinates" :qvehuan="qvehuan" @qvehuan1="handleCustomEvent" />
|
||||
<div v-show="tabbas == '1'" style="width:100%;height:600px;overflow: hidden;margin-top: 20px;">
|
||||
<MapChart :points="dynamicCoordinates" />
|
||||
</div>
|
||||
<div v-show="tabbas == '2'" style="width: 800px;height:600px;overflow: hidden;">
|
||||
<Echart :chart-data="lineData" />
|
||||
|
6
web/src/views/testdata/testtask/index.vue
vendored
6
web/src/views/testdata/testtask/index.vue
vendored
@ -91,7 +91,7 @@ function editproject(row: any) {
|
||||
//删除项目弹框
|
||||
function delproject(row: any) {
|
||||
ElMessageBox.confirm(
|
||||
'您确定要删除该项目吗?',
|
||||
'您确定要删除该项目及其中的节点和文件吗?',
|
||||
'警告',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
@ -100,6 +100,7 @@ function delproject(row: any) {
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
loading.value = true
|
||||
deleteTsTaskById({ id: row.id }).then((res: any) => {
|
||||
if (res.code == 0) {
|
||||
getdata()
|
||||
@ -123,7 +124,7 @@ function delprojectArr() {
|
||||
ids.push(item.id)
|
||||
})
|
||||
ElMessageBox.confirm(
|
||||
'您确定要删除该项目吗?',
|
||||
'您确定要删除这些项目及其中的节点和文件吗?',
|
||||
'警告',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
@ -132,6 +133,7 @@ function delprojectArr() {
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
loading.value = true
|
||||
deleteTsTaskByIds({ ids: ids.join(',') }).then((res: any) => {
|
||||
if (res.code == 0) {
|
||||
ElMessage({
|
||||
|
Loading…
Reference in New Issue
Block a user