Browse Source

文件结构调整

dev
jiangAB 3 months ago
parent
commit
6dacc2ba09
  1. 88
      public/map/components/CompanyShorePower.vue
  2. 140
      public/map/components/PortOverview.vue
  3. 189
      public/map/components/ShipShorePower.vue
  4. 130
      public/map/components/ShorePowerUsage.vue
  5. 0
      public/map/components/bk/components_index.vue
  6. 0
      public/map/components/bk/map_index.vue
  7. 0
      public/map/components/cesiumMap.vue
  8. 0
      public/map/components/charts/BarChart.vue
  9. 182
      public/map/components/charts/BarChartMinute.vue
  10. 15
      public/map/components/charts/LineChart.vue
  11. 0
      public/map/components/charts/PieChart.vue
  12. 240
      public/map/components/index.html
  13. 385
      public/map/index.vue

88
public/map/components/CompanyShorePower.vue

@ -0,0 +1,88 @@
<template>
<div class="company-shore-power">
<div class="left">
<div class="card digital-twin-card--deep-blue ">
<div class="card-title">
<div class="vertical-line"></div>
<img src="@/assets/svgs/data.svg" class="title-icon" />
<span class="title-text">港口企业岸电使用</span>
</div>
<div class="card-content">
<BarChart :chart-data="companyComparisonData" title="企业岸电使用对比" color="#4CAF50" />
</div>
</div>
<div class="card digital-twin-card--deep-blue ">
<div class="card-title">
<div class="vertical-line"></div>
<img src="@/assets/svgs/data.svg" class="title-icon" />
<span class="title-text">码头详情</span>
</div>
<div class="card-content">
<!-- 下拉框和饼图组件 -->
<div class="company-selector">
<el-select v-model="selectedCompany" placeholder="请选择公司" @change="handleCompanyChange"
style="width: 100%; margin-bottom: 20px;">
<el-option v-for="item in companyComparisonData" :key="item.name" :label="item.name" :value="item.name" />
</el-select>
<PieChart :chart-data="pieChartData" :title="`${selectedCompany}子项占比`" style="height: 300px;" />
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import BarChart from './charts/BarChart.vue'
import PieChart from './charts/PieChart.vue'
//
interface ChartDataItem {
name: string;
value: number;
}
interface Props {
companyComparisonData: ChartDataItem[];
}
const props = defineProps<Props>()
//
const emit = defineEmits<{
(e: 'company-change', company: string): void;
}>()
//
const selectedCompany = ref<string>(props.companyComparisonData[0]?.name || '')
//
const pieChartData = computed(() => {
//
return [
{ name: '码头1', value: Math.floor(Math.random() * 100) + 50 },
{ name: '码头2', value: Math.floor(Math.random() * 100) + 50 },
{ name: '码头3', value: Math.floor(Math.random() * 100) + 50 },
{ name: '码头4', value: Math.floor(Math.random() * 100) + 50 },
]
})
//
const handleCompanyChange = (company: string) => {
emit('company-change', company)
}
</script>
<style scoped>
.company-shore-power {
display: flex;
height: 100%;
gap: 10px;
}
.company-selector {
width: 100%;
}
</style>

140
public/map/components/PortOverview.vue

@ -0,0 +1,140 @@
<template>
<div class="port-overview">
<div class="left" style="width: 500px;">
<div class="card digital-twin-card--deep-blue ">
<div style="display: flex; align-items: center; justify-content: space-between;">
<div class="card-title">
<div class="vertical-line"></div>
<img src="@/assets/svgs/data.svg" class="title-icon" />
<span class="title-text">船舶状态</span>
</div>
<input class="search-container" type="text" placeholder="搜索船舶" />
</div>
<div class=" card-content">
<div class="ship-table">
<div class="ship-table-header">
<div class="ship-table-column ship-name-header">轮船名称</div>
<div class="ship-table-column ship-status-header">状态</div>
</div>
<div class="ship-table-body">
<div v-for="ship in shipStatusData" :key="ship.id" class="ship-table-row" @click="handleSwitch(ship)">
<div class="ship-table-column ship-name">
<div class="ship-icon">🚢</div>
<span class="ship-name-text">{{ ship.shipBasicInfo.name }}</span>
</div>
<div class="ship-table-column ship-status">
<div class="status-tag" :class="getStatusClass('正常')">
正常
</div>
<div class="status-tag" :class="getStatusClass('空闲')">
空闲
</div>
<div class="status-tag" :class="getStatusClass('故障')">
故障
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="right" style="width: 1000px;">
<div v-if="selectedShip" class="card digital-twin-card--deep-blue" style="flex: 1;">
<div class="card-title">
<div class="vertical-line"></div>
<img src="@/assets/svgs/data.svg" class="title-icon" />
<span class="title-text">{{ selectedShip.shipBasicInfo.name }}</span>
</div>
<div class="card-content">
<div class="overview-grid">
<div class="overview-item">
<div class="overview-value">{{ selectedShip.shorePowerAndShip?.powerUsage || 0 }}</div>
<div class="overview-label">功率kW</div>
</div>
<div class="overview-item">
<div class="overview-value">{{ shorePowerStatusMap[selectedShip.shorePowerAndShip?.status || ''] }}</div>
<div class="overview-label">岸电状态</div>
</div>
</div>
</div>
</div>
<div v-else class="card digital-twin-card--deep-blue" style="flex: 1;">
<div class="card-title">
<div class="vertical-line"></div>
<img src="@/assets/svgs/data.svg" class="title-icon" />
<span class="title-text">船舶详情</span>
</div>
<div class="card-content">
<div class="no-selection">请选择一艘船舶以查看详细信息</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
//
interface ShipItem {
id: string;
shipBasicInfo: {
name: string;
};
shorePowerAndShip?: {
status?: string;
powerUsage?: number;
};
modelInstance?: any;
}
interface Props {
shipStatusData: ShipItem[];
selectedShip: ShipItem | null;
}
const props = defineProps<Props>()
//
const emit = defineEmits<{
(e: 'switch-ship', ship: ShipItem): void;
}>()
//
const handleSwitch = (ship: ShipItem) => {
emit('switch-ship', ship)
}
//
const shorePowerStatusMap = {
'on': '使用中',
'off': '未使用',
'fault': '故障',
'': '未知'
}
//
const getStatusClass = (status: string) => {
return `status-${status === '使用中' ? 'shorepower' : status === '故障' ? 'fault' : 'default'}`
}
</script>
<style scoped>
.port-overview {
display: flex;
gap: 10px;
height: 100%;
}
.search-container {
height: 100%;
width: 200px;
border-radius: 4px;
border: 1px solid rgb(10, 130, 170);
padding: 0 10px;
margin-bottom: 8px;
background-color: rgba(255, 255, 255, 0.1);
}
</style>

189
public/map/components/ShipShorePower.vue

@ -0,0 +1,189 @@
<template>
<div class="ship-shore-power">
<div class="left" style="width: 800px;">
<div class="card digital-twin-card--deep-blue " style="flex: 1;">
<div class="card-title">
<div class="vertical-line"></div>
<img src="@/assets/svgs/data.svg" class="title-icon" />
<span class="title-text">概览</span>
</div>
<div class="card-content">
<div class="overview-grid">
<div class="overview-item">
<div class="overview-value">{{ berthingShips }}</div>
<div class="overview-label">在泊船舶数量</div>
</div>
<div class="overview-item">
<div class="overview-value">{{ shorePowerShips }}</div>
<div class="overview-label">使用岸电船舶数量</div>
</div>
<div class="overview-item">
<div class="overview-value">{{ noShorePowerShips }}</div>
<div class="overview-label">未使用岸电船舶数量</div>
</div>
</div>
</div>
</div>
<div class="card digital-twin-card--deep-blue " style="flex: 3;">
<div class="card-title">
<div class="vertical-line"></div>
<img src="@/assets/svgs/data.svg" class="title-icon" />
<span class="title-text">数据</span>
</div>
<div class="card-content">
<div class="ship-data-table-container" ref="scrollContainerRef">
<div class="ship-data-table">
<div v-for="(ship, index) in shipData" :key="index" class="ship-data-row">
<div class="ship-data-cell ship-name">{{ ship.name }}</div>
<div class="ship-data-cell wharf-name">{{ ship.wharf }}</div>
<div class="ship-data-cell berth-number">{{ ship.berth }}</div>
<div class="ship-data-cell shore-power-status" :class="ship.statusClass">{{ ship.status }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
//
interface ShipDataItem {
name: string;
wharf: string;
berth: string;
status: string;
statusClass: string;
}
interface Props {
berthingShips: number;
shorePowerShips: number;
noShorePowerShips: number;
shipData: ShipDataItem[];
}
const props = defineProps<Props>()
//
const scrollContainerRef = ref<HTMLElement | null>(null)
//
let scrollInterval: NodeJS.Timeout | null = null
const scrollSpeed = ref<number>(20) //
//
const startScroll = () => {
if (!scrollContainerRef.value) return
const container = scrollContainerRef.value
const content = container.querySelector('.ship-data-table')
if (!content) return
//
const scrollDistance = content.offsetHeight - container.clientHeight
if (scrollDistance <= 0) return //
let currentScroll = 0
let direction = 1 // 1 -1
//
if (scrollInterval) {
clearInterval(scrollInterval)
}
//
scrollInterval = setInterval(() => {
if (!scrollContainerRef.value) return
currentScroll += direction * scrollSpeed.value
//
if (currentScroll >= scrollDistance) {
currentScroll = scrollDistance
direction = -1
} else if (currentScroll <= 0) {
currentScroll = 0
direction = 1
}
//
scrollContainerRef.value.scrollTop = currentScroll
}, 100)
}
//
const stopScroll = () => {
if (scrollInterval) {
clearInterval(scrollInterval)
scrollInterval = null
}
}
// activeHeadGroup
watch(
() => props.shipData,
() => {
//
stopScroll()
startScroll()
},
{ deep: true }
)
//
onMounted(() => {
// DOM
setTimeout(() => {
startScroll()
}, 500)
})
//
onBeforeUnmount(() => {
stopScroll()
})
</script>
<style scoped>
.ship-shore-power {
display: flex;
height: 100%;
gap: 10px;
}
.ship-data-table-container {
height: 100%;
overflow-y: auto;
}
.ship-data-table {
width: 100%;
}
.ship-data-row {
display: flex;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.ship-data-cell {
flex: 1;
padding: 10px;
color: #fff;
}
.shore-power-status {
font-weight: bold;
}
.shore-power-status.status-on {
color: #4CAF50;
}
.shore-power-status.status-off {
color: #F44336;
}
</style>

130
public/map/components/ShorePowerUsage.vue

@ -0,0 +1,130 @@
<template>
<div class="shore-power-usage">
<div class="left" style="width: 40%;">
<div class="card digital-twin-card--deep-blue ">
<div class="card-title">
<div class="vertical-line"></div>
<img src="@/assets/svgs/data.svg" class="title-icon" />
<span class="title-text">总览</span>
</div>
<div class="card-content">
<div class="overview-grid">
<div class="overview-item">
<div class="overview-value">{{ totalPower }}</div>
<div class="overview-label">累计用电千瓦时</div>
</div>
<div class="overview-item">
<div class="overview-value">{{ fuelReduction }}</div>
<div class="overview-label">减少燃油</div>
</div>
<div class="overview-item">
<div class="overview-value">{{ co2Reduction }}</div>
<div class="overview-label">减少二氧化碳排放千克</div>
</div>
<div class="overview-item">
<div class="overview-value">{{ pm25Reduction }}</div>
<div class="overview-label">减少PM2.5排放千克</div>
</div>
<div class="overview-item">
<div class="overview-value">{{ noxReduction }}</div>
<div class="overview-label">减少氮氧化物千克</div>
</div>
<div class="overview-item">
<div class="overview-value">{{ so2Reduction }}</div>
<div class="overview-label">减少二氧化硫千克</div>
</div>
</div>
</div>
</div>
<div class="card digital-twin-card--deep-blue">
<div class="card-title">
<div class="vertical-line"></div>
<img src="@/assets/svgs/data.svg" class="title-icon" />
<span class="title-text">减少燃油</span>
</div>
<div class="card-content">
<LineChart :chart-data="fuelReductionData" title="减少燃油趋势" color="#4CAF50" />
</div>
</div>
<div class="card digital-twin-card--deep-blue">
<div class="card-title">
<div class="vertical-line"></div>
<img src="@/assets/svgs/data.svg" class="title-icon" />
<span class="title-text">减少CO排放千克</span>
</div>
<div class="card-content">
<LineChart :chart-data="co2ReductionData" title="减少CO₂排放趋势" color="#F44336" />
</div>
</div>
<div class="card digital-twin-card--deep-blue">
<div class="card-title">
<div class="vertical-line"></div>
<img src="@/assets/svgs/data.svg" class="title-icon" />
<span class="title-text">减少PM2.5排放千克</span>
</div>
<div class="card-content">
<LineChart :chart-data="pm25ReductionData" title="减少PM2.5排放趋势" color="#FF9800" />
</div>
</div>
<div class="card digital-twin-card--deep-blue">
<div class="card-title">
<div class="vertical-line"></div>
<img src="@/assets/svgs/data.svg" class="title-icon" />
<span class="title-text">减少NOₓ排放千克</span>
</div>
<div class="card-content">
<LineChart :chart-data="noxReductionData" title="减少NOₓ排放趋势" color="#9C27B0" />
</div>
</div>
<div class="card digital-twin-card--deep-blue">
<div class="card-title">
<div class="vertical-line"></div>
<img src="@/assets/svgs/data.svg" class="title-icon" />
<span class="title-text">减少SO排放千克</span>
</div>
<div class="card-content">
<LineChart :chart-data="so2ReductionData" title="减少SO₂排放趋势" color="#00BCD4" />
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import LineChart from './charts/LineChart.vue'
//
interface ChartDataItem {
name: string;
value: number;
}
interface Props {
totalPower: number;
fuelReduction: number;
co2Reduction: number;
pm25Reduction: number;
noxReduction: number;
so2Reduction: number;
fuelReductionData: ChartDataItem[];
co2ReductionData: ChartDataItem[];
pm25ReductionData: ChartDataItem[];
noxReductionData: ChartDataItem[];
so2ReductionData: ChartDataItem[];
}
defineProps<Props>()
</script>
<style scoped>
.shore-power-usage {
display: flex;
height: 100%;
gap: 10px;
}
</style>

0
public/map/components/_index.vue → public/map/components/bk/components_index.vue

0
public/map/_index.vue → public/map/components/bk/map_index.vue

0
public/map/components/index.vue → public/map/components/cesiumMap.vue

0
public/map/components/BarChart.vue → public/map/components/charts/BarChart.vue

182
public/map/components/charts/BarChartMinute.vue

@ -0,0 +1,182 @@
<template>
<div ref="chartContainer" class="bar-chart-minute-container"></div>
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
import * as echarts from 'echarts'
//
interface Props {
chartData?: Array<{ name: string; value: number }>
title?: string
color?: string
}
const props = withDefaults(defineProps<Props>(), {
chartData: () => [],
title: '',
color: '#1296db'
})
//
const chartContainer = ref<HTMLDivElement | null>(null)
let chartInstance: echarts.ECharts | null = null
//
const initChart = () => {
if (chartContainer.value) {
chartInstance = echarts.init(chartContainer.value)
updateChart()
}
}
//
const updateChart = () => {
if (!chartInstance || !props.chartData.length) return
const option = {
title: {
text: props.title,
textStyle: {
color: '#fff',
fontSize: 14
},
left: 'center'
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
backgroundColor: 'rgba(0, 0, 0, 0.7)',
borderColor: 'rgba(30, 120, 255, 0.4)',
borderWidth: 1,
textStyle: {
color: '#fff'
}
},
grid: {
left: '1%',
right: '1%',
top: '10%',
bottom: '1%',
containLabel: true
},
xAxis: {
type: 'category',
data: props.chartData.map(item => item.name),
axisLine: {
lineStyle: {
color: 'rgba(255, 255, 255, 0.3)'
}
},
axisLabel: {
color: 'rgba(255, 255, 255, 0.7)',
rotate: 0,
interval: Math.ceil(props.chartData.length / 24), //
formatter: (value: string) => {
// :00:00
if (value.endsWith(':00:00')) {
return value.split(':')[0] + '时'
}
return ''
},
margin: 10
},
axisTick: {
interval: Math.ceil(props.chartData.length / 24)
}
},
yAxis: {
type: 'value',
axisLine: {
lineStyle: {
color: 'rgba(255, 255, 255, 0.3)'
}
},
splitLine: {
lineStyle: {
color: 'rgba(255, 255, 255, 0.1)'
}
},
axisLabel: {
color: 'rgba(255, 255, 255, 0.7)'
}
},
series: [
{
type: 'bar',
data: props.chartData.map(item => item.value),
barWidth: 1, // 1px
itemStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: props.color
},
{
offset: 1,
color: props.color + '80'
}
]
},
borderRadius: [1, 1, 0, 0]
},
emphasis: {
itemStyle: {
color: props.color
}
}
}
]
}
chartInstance.setOption(option)
}
//
watch(
() => props.chartData,
() => {
updateChart()
},
{ deep: true }
)
//
const handleResize = () => {
if (chartInstance) {
chartInstance.resize()
}
}
//
onMounted(() => {
initChart()
window.addEventListener('resize', handleResize)
})
//
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize)
if (chartInstance) {
chartInstance.dispose()
chartInstance = null
}
})
</script>
<style scoped>
.bar-chart-minute-container {
width: 100%;
height: 100%;
min-height: 200px;
}
</style>

15
public/map/components/LineChart.vue → public/map/components/charts/LineChart.vue

@ -37,7 +37,7 @@ const updateChart = () => {
const option = { const option = {
title: { title: {
text: props.title, // text: props.title,
textStyle: { textStyle: {
color: '#fff', color: '#fff',
fontSize: 14 fontSize: 14
@ -113,10 +113,10 @@ const updateChart = () => {
} }
], ],
grid: { grid: {
left: '5%', left: '1%',
right: '5%', right: '1%',
top: '15%', top: '6%',
bottom: '10%', bottom: '1%',
containLabel: true containLabel: true
} }
} }
@ -142,7 +142,10 @@ const handleResize = () => {
// //
onMounted(() => { onMounted(() => {
// DOM
setTimeout(() => {
initChart() initChart()
}, 200)
window.addEventListener('resize', handleResize) window.addEventListener('resize', handleResize)
}) })
@ -160,6 +163,6 @@ onBeforeUnmount(() => {
.line-chart-container { .line-chart-container {
width: 100%; width: 100%;
height: 100%; height: 100%;
min-height: 200px; min-height: 0;
} }
</style> </style>

0
public/map/components/PieChart.vue → public/map/components/charts/PieChart.vue

240
public/map/components/index.html

@ -1,240 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>CesiumJS 天地图纯影像示例</title>
<link href="https://cesium.com/downloads/cesiumjs/releases/1.123/Build/Cesium/Widgets/widgets.css" rel="stylesheet">
<script src="https://cesium.com/downloads/cesiumjs/releases/1.123/Build/Cesium/Cesium.js"></script>
<style>
/* 确保 Cesium 容器占满整个浏览器窗口 */
body { margin: 0; overflow: hidden; }
#cesiumContainer { width: 100%; height: 100vh; }
/* 悬浮按钮样式 */
.control-btn {
position: absolute;
z-index: 1000;
padding: 10px 15px;
background-color: rgba(0, 0, 0, 0.7);
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
transition: background-color 0.3s;
}
.control-btn:hover {
background-color: rgba(0, 0, 0, 0.9);
}
#cameraInfoBtn {
top: 20px;
right: 20px;
}
/* 视角切换按钮 */
.view-buttons {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 10px;
z-index: 1000;
}
.view-btn {
padding: 8px 16px;
background-color: rgba(0, 0, 0, 0.7);
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
transition: background-color 0.3s;
}
.view-btn:hover {
background-color: rgba(0, 0, 0, 0.9);
}
</style>
</head>
<body>
<div id="cesiumContainer"></div>
<button id="cameraInfoBtn" class="control-btn">获取当前视角参数</button>
<!-- 视角切换按钮 -->
<div class="view-buttons">
<button class="view-btn" data-view="overview">全局视角</button>
<button class="view-btn" data-view="ship1">船1视角</button>
<button class="view-btn" data-view="ship2">船2视角</button>
<button class="view-btn" data-view="ship3">船3视角</button>
<button class="view-btn" data-view="ship4">船4视角</button>
</div>
<script type="module">
// ----------------------------------------------------------------------
// ** 请替换为您申请的天地图密钥 **
// ----------------------------------------------------------------------
const TDT_KEY = 'b19d3ad72716d1a28cf77836dfa19a71';
// 步骤 1: 初始化 Viewer 并配置为使用天地图影像和平坦地形
const viewer = new Cesium.Viewer('cesiumContainer', {
// **核心设置:禁用所有默认影像,我们将手动添加天地图**
imageryProvider: false,
// **使用平坦地形,避免依赖 Cesium Ion**
terrainProvider: new Cesium.EllipsoidTerrainProvider(),
// 禁用所有不必要的UI控件
timeline: false,
animation: false,
baseLayerPicker: false,
geocoder: false,
sceneModePicker: false,
navigationHelpButton: false,
infoBox: false,
fullscreenButton: false,
homeButton: false,
// 启用抗锯齿和其他渲染优化
contextOptions: {
webgl: {
antialias: true
}
}
});
// 禁用所有鼠标和键盘的相机控制操作
viewer.scene.screenSpaceCameraController.enableRotate = false;
viewer.scene.screenSpaceCameraController.enableTranslate = false;
viewer.scene.screenSpaceCameraController.enableZoom = false;
viewer.scene.screenSpaceCameraController.enableTilt = false;
viewer.scene.screenSpaceCameraController.enableLook = false;
// 渲染优化设置
// 启用抗锯齿
viewer.scene.postProcessStages.fxaa.enabled = true;
// 启用深度测试,确保模型能够被地形正确遮挡
viewer.scene.globe.depthTestAgainstTerrain = true;
// 步骤 2: 添加天地图卫星影像底图 (img: 卫星图层, w: WGS84坐标系)
const tdtImage = new Cesium.WebMapTileServiceImageryProvider({
// URL 中 LAYER=img 表示卫星影像图层
url: "http://t{s}.tianditu.gov.cn/img_w/wmts?service=wmts&request=GetTile&version=1.0.0&LAYER=img&tileMatrixSet=w&TileMatrix={TileMatrix}&TileRow={TileRow}&TileCol={TileCol}&style=default&format=tiles&tk=" + TDT_KEY,
subdomains: ['0', '1', '2', '3', '4', '5', '6', '7'],
layer: 'tdtImgLayer',
style: 'default',
format: 'tiles',
tileMatrixSetID: 'w', // 确保使用 WGS84 坐标系
maximumLevel: 18
});
// 步骤 3: 将图层添加到 Viewer 中
viewer.imageryLayers.addImageryProvider(tdtImage);
// **注意:由于您要求不添加标注层,因此这里不添加 tdtCVA 或 tdtCIA**
// 引入我们自定义的标记和定位功能
import { initMarkerAndPosition } from './src/cesium-utils.js';
// 初始化标记和定位
initMarkerAndPosition(viewer);
// 定义预设视角参数
const presetViews = {
overview: {
destination: Cesium.Cartesian3.fromDegrees(118.4603328835826, 38.953967794772765, 560.0105923892418),
orientation: {
heading: Cesium.Math.toRadians(231.0260194269599),
pitch: Cesium.Math.toRadians(-24.749471814600415),
roll: Cesium.Math.toRadians(0.005508519138937096)
}
},
ship1: {
destination: Cesium.Cartesian3.fromDegrees(118.45467237056748 + 0.005, 38.94345692673452, 150), // 在船的前方
orientation: {
heading: Cesium.Math.toRadians(212), // 朝向船的位置 (180+32)
pitch: Cesium.Math.toRadians(-15),
roll: 0
}
},
ship2: {
destination: Cesium.Cartesian3.fromDegrees(118.45183602217774 + 0.005, 38.94485094840323, 150), // 在船的前方
orientation: {
heading: Cesium.Math.toRadians(212), // 朝向船的位置 (180+32)
pitch: Cesium.Math.toRadians(-15),
roll: 0
}
},
ship3: {
destination: Cesium.Cartesian3.fromDegrees(118.4468305964142 + 0.005, 38.947237470602076, 150), // 在船的前方
orientation: {
heading: Cesium.Math.toRadians(212), // 朝向船的位置 (180+32)
pitch: Cesium.Math.toRadians(-15),
roll: 0
}
},
ship4: {
destination: Cesium.Cartesian3.fromDegrees(118.44446808752532 + 0.005, 38.94835610136433, 150), // 在船的前方
orientation: {
heading: Cesium.Math.toRadians(212), // 朝向船的位置 (180+32)
pitch: Cesium.Math.toRadians(-15),
roll: 0
}
}
};
// 设置初始视角
viewer.camera.setView(presetViews.overview);
// 添加按钮点击事件监听器
document.getElementById('cameraInfoBtn').addEventListener('click', function() {
// 获取当前相机位置
const camera = viewer.camera;
const position = camera.position;
const cartographic = Cesium.Cartographic.fromCartesian(position);
// 转换为度数
const longitude = Cesium.Math.toDegrees(cartographic.longitude);
const latitude = Cesium.Math.toDegrees(cartographic.latitude);
const height = cartographic.height;
// 获取相机方向
const heading = Cesium.Math.toDegrees(camera.heading);
const pitch = Cesium.Math.toDegrees(camera.pitch);
const roll = Cesium.Math.toDegrees(camera.roll);
// 输出到控制台
console.log('当前相机视角参数:');
console.log('位置 - 经度:', longitude, '纬度:', latitude, '高度:', height);
console.log('方向 - 航向角:', heading, '俯仰角:', pitch, '翻滚角:', roll);
// 显示提示信息
alert('相机参数已输出到控制台,请打开开发者工具查看。');
});
// 添加视角切换按钮事件监听器
document.querySelectorAll('.view-btn').forEach(button => {
button.addEventListener('click', function() {
const viewName = this.getAttribute('data-view');
const viewParams = presetViews[viewName];
if (viewParams) {
viewer.camera.flyTo({
destination: viewParams.destination,
orientation: viewParams.orientation,
duration: 2.0 // 动画持续时间(秒)
});
}
});
});
</script>
</body>
</html>

385
public/map/index.vue

@ -1,8 +1,6 @@
<template> <template>
<div> <div>
<PublicMapComponents ref="mapComponentRef" class="map-base" :defCenter="[38.8417433306111, 118.43845367431642]" <PublicMapComponents ref="mapComponentRef" class="map-base" />
:defZoom="12" />
<!-- <div class="show-data"> -->
<div class="head"> <div class="head">
<div class="head-title"> <div class="head-title">
<span>曹妃甸港区船舶岸电监管平台</span> <span>曹妃甸港区船舶岸电监管平台</span>
@ -21,324 +19,29 @@
<!-- 港区概览 --> <!-- 港区概览 -->
<template v-if="activeHeadGroup === 0"> <template v-if="activeHeadGroup === 0">
<div class="left"> <PortOverview :ship-status-data="shipStatusData" :selected-ship="selectedShip" @select-ship="handleSwitch"
<div class="card digital-twin-card--deep-blue "> :get-status-class="getStatusClass" :get-shore-power-status-text="getShorePowerStatusText" />
<div class="card-title">
<div class="vertical-line"></div>
<img src="@/assets/svgs/data.svg" class="title-icon" />
<span class="title-text">船舶状态</span>
</div>
<div class="card-content">
<div class="ship-table">
<div class="ship-table-header">
<div class="ship-table-column ship-name-header">轮船名称</div>
<div class="ship-table-column ship-status-header">状态</div>
</div>
<div class="ship-table-body">
<div v-for="ship in shipStatusData" :key="ship.id" class="ship-table-row" @click="handleSwitch(ship)">
<div class="ship-table-column ship-name">
<div class="ship-icon">🚢</div>
<span class="ship-name-text">{{ ship.shipBasicInfo.name }}</span>
</div>
<div class="ship-table-column ship-status">
<div class="status-tag" :class="getStatusClass('正常')">
正常
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="right">
<div v-if="selectedShip" class="card digital-twin-card--deep-blue" style="flex: 2;">
<div class="card-title">
<div class="vertical-line"></div>
<img src="@/assets/svgs/data.svg" class="title-icon" />
<span class="title-text">船舶详情</span>
</div>
<div class="card-content">
<div class="ship-detail">
<div class="detail-item">
<span class="label">船名:</span>
<span class="value">{{ selectedShip.shipBasicInfo.name }}</span>
</div>
<div class="detail-item">
<span class="label">英文船名:</span>
<span class="value">{{ selectedShip.shipBasicInfo.nameEn }}</span>
</div>
<div class="detail-item">
<span class="label">船舶呼号:</span>
<span class="value">{{ selectedShip.shipBasicInfo.callSign }}</span>
</div>
<div class="detail-item">
<span class="label">船长:</span>
<span class="value">{{ selectedShip.shipBasicInfo.length }} </span>
</div>
<div class="detail-item">
<span class="label">船宽:</span>
<span class="value">{{ selectedShip.shipBasicInfo.width }} </span>
</div>
<div class="detail-item">
<span class="label">满载吃水深度:</span>
<span class="value">{{ selectedShip.shipBasicInfo.fullLoadDraft }} </span>
</div>
<div class="detail-item">
<span class="label">吨位:</span>
<span class="value">{{ selectedShip.shipBasicInfo.tonnage }} </span>
</div>
<div class="detail-item">
<span class="label">航运单位:</span>
<span class="value">{{ selectedShip.shipBasicInfo.shippingCompany }}</span>
</div>
<div class="detail-item">
<span class="label">岸电联系人:</span>
<span class="value">{{ selectedShip.shipBasicInfo.shorePowerContact }}</span>
</div>
<div class="detail-item">
<span class="label">联系方式:</span>
<span class="value">{{ selectedShip.shipBasicInfo.shorePowerContactPhone }}</span>
</div>
<div class="detail-item">
<span class="label">船检登记号:</span>
<span class="value">{{ selectedShip.shipBasicInfo.inspectionNo }}</span>
</div>
<div class="detail-item">
<span class="label">创建时间:</span>
<span class="value">{{ new Date(selectedShip.shipBasicInfo.createTime).toLocaleString() }}</span>
</div>
</div>
</div>
</div>
<div v-if="selectedShip && selectedShip.shorePowerAndShip" class="card digital-twin-card--deep-blue"
style="flex: 1;">
<div class="card-title">
<div class="vertical-line"></div>
<img src="@/assets/svgs/data.svg" class="title-icon" />
<span class="title-text">岸电信息</span>
</div>
<div class="card-content">
<div class="ship-detail">
<div class="detail-item">
<span class="label">停泊状态:</span>
<span class="value">{{ selectedShip.shorePowerAndShip.type === 'left' ? '左舷停泊' :
selectedShip.shorePowerAndShip.type === 'right' ? '右舷停泊' : selectedShip.shorePowerAndShip.type
}}</span>
</div>
<div class="detail-item">
<span class="label">靠泊状态:</span>
<span class="value">{{ getShorePowerStatusText(selectedShip.shorePowerAndShip.status) }}</span>
</div>
</div>
</div>
</div>
<div v-if="selectedShip && !selectedShip.shorePowerAndShip" class="card digital-twin-card--deep-blue">
<div class="card-title">
<div class="vertical-line"></div>
<img src="@/assets/svgs/data.svg" class="title-icon" />
<span class="title-text">岸电信息</span>
</div>
<div class="card-content">
<div class="no-selection">暂无岸电信息</div>
</div>
</div>
<div v-if="!selectedShip" class="card digital-twin-card--deep-blue">
<div class="card-title">
<div class="vertical-line"></div>
<img src="@/assets/svgs/data.svg" class="title-icon" />
<span class="title-text">船舶详情</span>
</div>
<div class="card-content">
<div class="no-selection">请选择一艘船舶以查看详细信息</div>
</div>
</div>
</div>
</template> </template>
<!-- 港口岸电使用情况 --> <!-- 港口岸电使用情况 -->
<template v-if="activeHeadGroup === 1"> <template v-if="activeHeadGroup === 1">
<div class="left"> <ShorePowerUsage :total-power="totalPower" :fuel-reduction="fuelReduction" :co2-reduction="co2Reduction"
<div class="card digital-twin-card--deep-blue "> :pm25-reduction="pm25Reduction" :nox-reduction="noxReduction" :so2-reduction="so2Reduction"
<div class="card-title"> :fuel-reduction-data="fuelReductionData" :co2-reduction-data="co2ReductionData"
<div class="vertical-line"></div> :pm25-reduction-data="pm25ReductionData" :nox-reduction-data="noxReductionData"
<img src="@/assets/svgs/data.svg" class="title-icon" /> :so2-reduction-data="so2ReductionData" />
<span class="title-text">总览</span>
</div>
<div class="card-content">
<div class="overview-grid">
<div class="overview-item">
<div class="overview-value">{{ totalPower }}</div>
<div class="overview-label">累计用电千瓦时</div>
</div>
<div class="overview-item">
<div class="overview-value">{{ fuelReduction }}</div>
<div class="overview-label">减少燃油</div>
</div>
<div class="overview-item">
<div class="overview-value">{{ co2Reduction }}</div>
<div class="overview-label">减少二氧化碳排放千克</div>
</div>
<div class="overview-item">
<div class="overview-value">{{ pm25Reduction }}</div>
<div class="overview-label">减少PM2.5排放千克</div>
</div>
<div class="overview-item">
<div class="overview-value">{{ noxReduction }}</div>
<div class="overview-label">减少氮氧化物千克</div>
</div>
<div class="overview-item">
<div class="overview-value">{{ so2Reduction }}</div>
<div class="overview-label">减少二氧化硫千克</div>
</div>
</div>
</div>
</div>
<div class="card digital-twin-card--deep-blue">
<div class="card-title">
<div class="vertical-line"></div>
<img src="@/assets/svgs/data.svg" class="title-icon" />
<span class="title-text">减少燃油</span>
</div>
<div class="card-content">
<LineChart :chart-data="fuelReductionData" title="减少燃油趋势" color="#4CAF50" style="height: 200px;" />
</div>
</div>
<div class="card digital-twin-card--deep-blue">
<div class="card-title">
<div class="vertical-line"></div>
<img src="@/assets/svgs/data.svg" class="title-icon" />
<span class="title-text">减少二氧化碳排放千克</span>
</div>
<div class="card-content">
<LineChart :chart-data="co2ReductionData" title="减少CO₂排放趋势" color="#F44336" style="height: 200px;" />
</div>
</div>
</div>
<div class="right">
<div class="card digital-twin-card--deep-blue">
<div class="card-title">
<div class="vertical-line"></div>
<img src="@/assets/svgs/data.svg" class="title-icon" />
<span class="title-text">减少PM2.5排放千克</span>
</div>
<div class="card-content">
<LineChart :chart-data="pm25ReductionData" title="减少PM2.5排放趋势" color="#FF9800" style="height: 200px;" />
</div>
</div>
<div class="card digital-twin-card--deep-blue">
<div class="card-title">
<div class="vertical-line"></div>
<img src="@/assets/svgs/data.svg" class="title-icon" />
<span class="title-text">减少氮氧化物千克</span>
</div>
<div class="card-content">
<LineChart :chart-data="noxReductionData" title="减少NOₓ排放趋势" color="#9C27B0" style="height: 200px;" />
</div>
</div>
<div class="card digital-twin-card--deep-blue">
<div class="card-title">
<div class="vertical-line"></div>
<img src="@/assets/svgs/data.svg" class="title-icon" />
<span class="title-text">减少二氧化硫千克</span>
</div>
<div class="card-content">
<LineChart :chart-data="so2ReductionData" title="减少SO₂排放趋势" color="#00BCD4" style="height: 200px;" />
</div>
</div>
</div>
</template> </template>
<!-- 港口企业岸电使用 --> <!-- 港口企业岸电使用 -->
<template v-if="activeHeadGroup === 2"> <template v-if="activeHeadGroup === 2">
<div class="left"> <CompanyShorePower :company-comparison-data="companyComparisonData" :selected-company="selectedCompany"
<div class="card digital-twin-card--deep-blue "> :pie-chart-data="pieChartData" @update:selected-company="selectedCompany = $event" />
<div class="card-title">
<div class="vertical-line"></div>
<img src="@/assets/svgs/data.svg" class="title-icon" />
<span class="title-text">港口企业岸电使用</span>
</div>
<div class="card-content">
<BarChart :chart-data="companyComparisonData" title="企业岸电使用对比" color="#4CAF50" style="height: 300px;" />
</div>
</div>
<div class="card digital-twin-card--deep-blue ">
<div class="card-title">
<div class="vertical-line"></div>
<img src="@/assets/svgs/data.svg" class="title-icon" />
<span class="title-text">码头详情</span>
</div>
<div class="card-content">
<!-- 下拉框和饼图组件 -->
<div class="company-selector">
<el-select v-model="selectedCompany" placeholder="请选择公司" @change="handleCompanyChange"
style="width: 100%; margin-bottom: 20px;">
<el-option v-for="item in companyComparisonData" :key="item.name" :label="item.name"
:value="item.name" />
</el-select>
<PieChart :chart-data="pieChartData" :title="`${selectedCompany}子项占比`" style="height: 300px;" />
</div>
</div>
</div>
</div>
</template> </template>
<!-- 船舶岸电使用情况 --> <!-- 船舶岸电使用情况 -->
<template v-if="activeHeadGroup === 3"> <template v-if="activeHeadGroup === 3">
<div class="left" style="width: 800px;"> <ShipShorePower :berthing-ships="berthingShips" :shore-power-ships="shorePowerShips"
<div class="card digital-twin-card--deep-blue " style="flex: 1;"> :no-shore-power-ships="noShorePowerShips" :ship-data="shipData" />
<div class="card-title">
<div class="vertical-line"></div>
<img src="@/assets/svgs/data.svg" class="title-icon" />
<span class="title-text">概览</span>
</div>
<div class="card-content">
<div class="overview-grid">
<div class="overview-item">
<div class="overview-value">{{ berthingShips }}</div>
<div class="overview-label">在泊船舶数量</div>
</div>
<div class="overview-item">
<div class="overview-value">{{ shorePowerShips }}</div>
<div class="overview-label">使用岸电船舶数量</div>
</div>
<div class="overview-item">
<div class="overview-value">{{ noShorePowerShips }}</div>
<div class="overview-label">未使用岸电船舶数量</div>
</div>
</div>
</div>
</div>
<div class="card digital-twin-card--deep-blue " style="flex: 3;">
<div class="card-title">
<div class="vertical-line"></div>
<img src="@/assets/svgs/data.svg" class="title-icon" />
<span class="title-text">数据</span>
</div>
<div class="card-content">
<div class="ship-data-table-container" ref="scrollContainerRef">
<div class="ship-data-table">
<div v-for="(ship, index) in shipData" :key="index" class="ship-data-row">
<div class="ship-data-cell ship-name">{{ ship.name }}</div>
<div class="ship-data-cell wharf-name">{{ ship.wharf }}</div>
<div class="ship-data-cell berth-number">{{ ship.berth }}</div>
<div class="ship-data-cell shore-power-status" :class="ship.statusClass">{{ ship.status }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- <div class="right" style="width: 600px;">
</div> -->
</template> </template>
@ -347,10 +50,11 @@
<script setup lang="ts"> <script setup lang="ts">
import PublicMapComponents from './components/index.vue' import PublicMapComponents from './components/cesiumMap.vue'
import LineChart from './components/LineChart.vue' import PortOverview from './components/PortOverview.vue'
import BarChart from './components/BarChart.vue' import ShorePowerUsage from './components/ShorePowerUsage.vue'
import PieChart from './components/PieChart.vue' import CompanyShorePower from './components/CompanyShorePower.vue'
import ShipShorePower from './components/ShipShorePower.vue'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { onMounted, onUnmounted, ref, computed, watch } from 'vue' import { onMounted, onUnmounted, ref, computed, watch } from 'vue'
import { MapApi } from "@/api/shorepower/map"; import { MapApi } from "@/api/shorepower/map";
@ -862,14 +566,18 @@ const getStatusClass = (status: string) => {
switch (status) { switch (status) {
case '正常': case '正常':
return 'status-normal' return 'status-normal'
case '空闲':
return 'status-idle'
case '故障':
return 'status-fault'
case '异常': case '异常':
return 'status-abnormal' return 'status-abnormal'
case '维修中': case '维修中':
return 'status-maintenance' return 'status-maintenance'
case '岸电使用中': case '岸电使用中':
return 'status-shorepower' return 'status-shorepower'
case '岸电故障': // case '':
return 'status-fault' // return 'status-fault'
default: default:
return 'status-default' return 'status-default'
} }
@ -947,6 +655,16 @@ const getStatusClass = (status: string) => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
.search-container {
height: 100%;
width: 200px;
border-radius: 4px;
border: 1px solid rgb(10, 130, 170);
padding: 0 10px;
margin-bottom: 8px;
background-color: rgba(255, 255, 255, 0.1);
}
.card-title { .card-title {
font-size: 18px; font-size: 18px;
font-weight: 600; font-weight: 600;
@ -969,7 +687,7 @@ const getStatusClass = (status: string) => {
} }
.title-text { .title-text {
font-size: 16px; font-size: 18px;
color: #FFF; color: #FFF;
font-weight: 500; font-weight: 500;
} }
@ -978,6 +696,8 @@ const getStatusClass = (status: string) => {
.card-content { .card-content {
flex: 1; flex: 1;
min-height: 0; min-height: 0;
width: 100%;
// padding: 10px;
} }
.overview-grid { .overview-grid {
@ -1012,7 +732,7 @@ const getStatusClass = (status: string) => {
} }
/* 船舶数据表格样式 */ /* 船舶数据表格样式 */
.card-content { .ship-data-table-container .card-content {
height: 100%; height: 100%;
padding: 10px; padding: 10px;
} }
@ -1166,7 +886,7 @@ const getStatusClass = (status: string) => {
left: 0px; left: 0px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 20px; gap: 8px;
overflow-y: auto; overflow-y: auto;
} }
@ -1179,7 +899,7 @@ const getStatusClass = (status: string) => {
right: 0px; right: 0px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 20px; gap: 8px;
overflow-y: auto; overflow-y: auto;
} }
@ -1206,6 +926,7 @@ const getStatusClass = (status: string) => {
justify-content: space-between; justify-content: space-between;
padding: 4px 0; padding: 4px 0;
border-bottom: 1px solid rgba(30, 120, 255, 0.2); border-bottom: 1px solid rgba(30, 120, 255, 0.2);
font-size: 24px;
} }
.detail-item:last-child { .detail-item:last-child {
@ -1240,7 +961,7 @@ const getStatusClass = (status: string) => {
background-color: rgba(17, 138, 237, 0.3); background-color: rgba(17, 138, 237, 0.3);
border-bottom: 1px solid rgba(30, 120, 255, 0.4); border-bottom: 1px solid rgba(30, 120, 255, 0.4);
font-weight: bold; font-weight: bold;
font-size: 14px; font-size: 18px;
} }
.ship-table-body { .ship-table-body {
@ -1252,7 +973,7 @@ const getStatusClass = (status: string) => {
.ship-table-row { .ship-table-row {
display: flex; display: flex;
cursor: pointer; cursor: pointer;
padding: 3px 6px; padding: 8px 8px;
border-bottom: 1px solid rgba(30, 120, 255, 0.2); border-bottom: 1px solid rgba(30, 120, 255, 0.2);
transition: background-color 0.2s ease; transition: background-color 0.2s ease;
} }
@ -1286,7 +1007,7 @@ const getStatusClass = (status: string) => {
} }
.ship-name-text { .ship-name-text {
font-size: 14px; font-size: 18px;
color: #e6f2ff; color: #e6f2ff;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
@ -1303,7 +1024,7 @@ const getStatusClass = (status: string) => {
.status-tag { .status-tag {
padding: 2px 4px; padding: 2px 4px;
border-radius: 4px; border-radius: 4px;
font-size: 12px; font-size: 16px;
font-weight: 500; font-weight: 500;
color: #fff; color: #fff;
text-shadow: 0 0 3px rgba(0, 0, 0, 0.5); text-shadow: 0 0 3px rgba(0, 0, 0, 0.5);
@ -1314,6 +1035,20 @@ const getStatusClass = (status: string) => {
box-shadow: 0 0 5px rgba(76, 175, 80, 0.5); box-shadow: 0 0 5px rgba(76, 175, 80, 0.5);
} }
/* 空闲状态 */
.status-idle {
background-color: #2196F3;
/* 蓝色 */
box-shadow: 0 0 5px rgba(33, 150, 243, 0.5);
}
/* 故障状态 */
.status-fault {
background-color: #F44336;
/* 红色 */
box-shadow: 0 0 5px rgba(244, 67, 54, 0.5);
}
.status-abnormal { .status-abnormal {
background-color: #F44336; background-color: #F44336;
box-shadow: 0 0 5px rgba(244, 67, 54, 0.5); box-shadow: 0 0 5px rgba(244, 67, 54, 0.5);
@ -1329,10 +1064,10 @@ const getStatusClass = (status: string) => {
box-shadow: 0 0 5px rgba(33, 150, 243, 0.5); box-shadow: 0 0 5px rgba(33, 150, 243, 0.5);
} }
.status-fault { /* .status-fault {
background-color: #9C27B0; background-color: #9C27B0;
box-shadow: 0 0 5px rgba(156, 39, 176, 0.5); box-shadow: 0 0 5px rgba(156, 39, 176, 0.5);
} } */
.status-default { .status-default {
background-color: #607D8B; background-color: #607D8B;

Loading…
Cancel
Save