jiangAB 1 month ago
parent
commit
75fecb5816
  1. 3
      .eslintrc.js
  2. BIN
      public.zip
  3. BIN
      public/map/.index.vue.swp
  4. 50
      public/map/components/CompanyShorePower.vue
  5. 168
      public/map/components/PortOverview.vue
  6. 107
      public/map/components/ShipShorePower.vue
  7. 400
      public/map/components/ShorePowerUsage.vue
  8. 163
      public/map/components/cesiumMap.vue
  9. 18
      public/map/components/charts/BarChartMinute.vue
  10. 4
      public/map/components/charts/LineChart.vue
  11. 79
      public/map/components/charts/PieChart.vue
  12. 381
      public/map/index.vue

3
.eslintrc.js

@ -70,6 +70,7 @@ module.exports = defineConfig({
'vue/no-v-html': 'off',
'prettier/prettier': 'off', // 芋艿:默认关闭 prettier 的 ESLint 校验,因为我们使用的是 IDE 的 Prettier 插件
'@unocss/order': 'off', // 芋艿:禁用 unocss 【css】顺序的提示,因为暂时不需要这么严格,警告也有点繁琐
'@unocss/order-attributify': 'off' // 芋艿:禁用 unocss 【属性】顺序的提示,因为暂时不需要这么严格,警告也有点繁琐
'@unocss/order-attributify': 'off', // 芋艿:禁用 unocss 【属性】顺序的提示,因为暂时不需要这么严格,警告也有点繁琐
'vue/first-attribute-linebreak': 'off' // 禁用属性换行检查,解决与格式化工具的冲突
}
})

BIN
public.zip

Binary file not shown.

BIN
public/map/.index.vue.swp

Binary file not shown.

50
public/map/components/CompanyShorePower.vue

@ -1,7 +1,7 @@
<template>
<div class="company-shore-power">
<div class="left">
<div class="card digital-twin-card--deep-blue ">
<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" />
@ -10,7 +10,7 @@
<div class="card-content">
<BarChart :chart-data="companyComparisonData" title="企业岸电使用对比" color="#4CAF50" />
</div>
</div>
</div> -->
<div class="card digital-twin-card--deep-blue ">
<div class="card-title">
<div class="vertical-line"></div>
@ -25,7 +25,7 @@
<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;" />
<PieChart :chart-data="pieChartData" :title="`${selectedCompany}子项占比`" />
</div>
</div>
</div>
@ -35,13 +35,13 @@
<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;
children?: ChartDataItem[];
}
interface Props {
@ -50,28 +50,19 @@ interface Props {
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 selectedCompanyData = props.companyComparisonData.find(item => item.name === selectedCompany.value)
return selectedCompanyData?.children || []
})
//
const handleCompanyChange = (company: string) => {
emit('company-change', company)
selectedCompany.value = company
}
</script>
@ -82,7 +73,28 @@ const handleCompanyChange = (company: string) => {
gap: 10px;
}
.left {
display: flex;
flex-direction: column;
height: 100%;
}
.card {
display: flex;
flex-direction: column;
height: 100%;
}
.card-content {
flex: 1;
height: 100%;
min-height: 0;
}
.company-selector {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
</style>

168
public/map/components/PortOverview.vue

@ -8,7 +8,8 @@
<img src="@/assets/svgs/data.svg" class="title-icon" />
<span class="title-text">船舶状态</span>
</div>
<input class="search-container" type="text" placeholder="搜索船舶" />
<input class="search-container" type="text" placeholder="搜索船舶" v-model="searchKeyword"
@input="handleSearch" />
</div>
<div class=" card-content">
@ -18,7 +19,8 @@
<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 v-for="ship in filteredShipStatusData" :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>
@ -41,25 +43,89 @@
</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 v-if="selectedShip" class="right-two-row">
<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="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 class="overview-item">
<div class="overview-value">{{ shorePowerStatusMap[selectedShip.shorePowerAndShip?.status || ''] }}</div>
<div class="overview-label">岸电状态</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">
<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>
<div v-else class="card digital-twin-card--deep-blue" style="flex: 1;">
<div class="card-title">
<div class="vertical-line"></div>
@ -75,7 +141,7 @@
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { ref, computed } from 'vue'
//
interface ShipItem {
@ -97,27 +163,74 @@ interface Props {
const props = defineProps<Props>()
//
const searchKeyword = ref('')
//
const filteredShipStatusData = computed(() => {
if (!searchKeyword.value) {
return props.shipStatusData
}
return props.shipStatusData.filter(ship =>
ship.shipBasicInfo.name.toLowerCase().includes(searchKeyword.value.toLowerCase())
)
})
//
const handleSearch = () => {
//
}
//
const emit = defineEmits<{
(e: 'switch-ship', ship: ShipItem): void;
(e: 'item-click', ship: ShipItem): void;
}>()
//
const handleSwitch = (ship: ShipItem) => {
// console.log(ship)
emit('switch-ship', ship)
emit('item-click', ship)
}
//
const shorePowerStatusMap = {
'on': '使用中',
'off': '未使用',
'fault': '故障',
'': '未知'
1: '待靠泊',
2: '靠泊中',
3: '岸电接入中',
4: '用电中',
5: '岸电卸载中',
6: '岸电卸载完成',
7: '离泊',
9: '未使用岸电'
}
//
const getShorePowerStatusText = (status: number | string) => {
const statusNum = Number(status);
console.log(status)
return shorePowerStatusMap[statusNum] || status;
}
//
const getStatusClass = (status: string) => {
return `status-${status === '使用中' ? 'shorepower' : status === '故障' ? 'fault' : 'default'}`
switch (status) {
case '正常':
return 'status-normal'
case '空闲':
return 'status-idle'
case '故障':
return 'status-fault'
case '异常':
return 'status-abnormal'
case '维修中':
return 'status-maintenance'
case '岸电使用中':
return 'status-shorepower'
// case '':
// return 'status-fault'
default:
return 'status-default'
}
}
</script>
@ -128,11 +241,18 @@ const getStatusClass = (status: string) => {
height: 100%;
}
.right-two-row {
display: flex;
gap: 10px;
height: 100%;
}
.search-container {
height: 100%;
width: 200px;
border-radius: 4px;
border: 1px solid rgb(10, 130, 170);
color: #FFF;
padding: 0 10px;
margin-bottom: 8px;
background-color: rgba(255, 255, 255, 0.1);

107
public/map/components/ShipShorePower.vue

@ -1,6 +1,6 @@
<template>
<div class="ship-shore-power">
<div class="left" style="width: 800px;">
<div class="left" style="width: 50%;">
<div class="card digital-twin-card--deep-blue " style="flex: 1;">
<div class="card-title">
<div class="vertical-line"></div>
@ -31,13 +31,13 @@
<span class="title-text">数据</span>
</div>
<div class="card-content">
<div class="ship-data-table-container" ref="scrollContainerRef">
<div class="ship-data-table-container">
<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 class="ship-data-cell content shore-power-status" :class="ship.statusClass">{{ ship.status }}</div>
</div>
</div>
</div>
@ -48,7 +48,7 @@
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
// import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
//
interface ShipDataItem {
@ -67,85 +67,6 @@ interface Props {
}
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>
@ -171,8 +92,13 @@ onBeforeUnmount(() => {
.ship-data-cell {
flex: 1;
padding: 10px;
color: #fff;
padding: 1px;
font-size: 16px;
/* color: #fff; */
}
.ship-data-cell.content {
flex: 2;
}
.shore-power-status {
@ -186,4 +112,15 @@ onBeforeUnmount(() => {
.shore-power-status.status-off {
color: #F44336;
}
.overview-value {
font-size: 24px;
font-weight: 700;
}
.overview-label {
font-size: 18px;
font-weight: 700;
}
</style>

400
public/map/components/ShorePowerUsage.vue

@ -1,94 +1,165 @@
<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>
<!-- Average Mode -->
<template v-if="displayMode === 'average'">
<div class="left" style="width: 40%;">
<div v-for="card in cards.slice(0, 3)" :key="card.id" class="card digital-twin-card--deep-blue"
@click="handleSelectCard(card.id)">
<div class="card-title">
<div class="vertical-line"></div>
<img src="@/assets/svgs/data.svg" class="title-icon" />
<span class="title-text">{{ card.title }}</span>
</div>
<div class="card-content">
<div v-if="card.type === 'overview'" 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>
<BarChartMinute v-else :chart-data="commonChartData" :title="card.title" :color="card.color" />
</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 class="right" style="width: 40%;">
<div v-for="card in cards.slice(3)" :key="card.id" class="card digital-twin-card--deep-blue"
@click="handleSelectCard(card.id)">
<div class="card-title">
<div class="vertical-line"></div>
<img src="@/assets/svgs/data.svg" class="title-icon" />
<span class="title-text">{{ card.title }}</span>
</div>
<div class="card-content">
<div v-if="card.type === 'overview'" 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>
<BarChartMinute v-else :chart-data="commonChartData" :title="card.title" :color="card.color" />
</div>
</div>
</div>
</template>
<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" />
<!-- Magnify Mode -->
<div v-if="displayMode === 'magnify'" class="magnify">
<div class="big">
<div v-for="card in cards.filter(c => c.id === selectedCard)" :key="card.id"
class="card digital-twin-card--deep-blue" @click="handleSelectCard(card.id)">
<div class="card-title">
<div class="vertical-line"></div>
<img src="@/assets/svgs/data.svg" class="title-icon" />
<span class="title-text">{{ card.title }}</span>
</div>
<div class="card-content">
<div v-if="card.type === 'overview'" 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>
<BarChartMinute v-else :chart-data="commonChartData" :title="card.title" :color="card.color" />
</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">减少SO排放千克</span>
</div>
<div class="card-content">
<LineChart :chart-data="so2ReductionData" title="减少SO₂排放趋势" color="#00BCD4" />
<div class="one-row">
<div v-for="card in cards.filter(c => c.id !== selectedCard)" :key="card.id"
class="card digital-twin-card--deep-blue" @click="handleSelectCard(card.id)">
<div class="card-title">
<div class="vertical-line"></div>
<img src="@/assets/svgs/data.svg" class="title-icon" />
<span class="title-text">{{ card.title }}</span>
</div>
<div class="card-content">
<div v-if="card.type === 'overview'" 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>
<BarChartMinute v-else :chart-data="commonChartData" :title="card.title" :color="card.color" />
</div>
</div>
</div>
</div>
@ -96,7 +167,8 @@
</template>
<script setup lang="ts">
import LineChart from './charts/LineChart.vue'
import { ref, onMounted, onBeforeUnmount } from 'vue'
import BarChartMinute from './charts/BarChartMinute.vue'
//
interface ChartDataItem {
@ -104,6 +176,14 @@ interface ChartDataItem {
value: number;
}
interface CardInfo {
id: string;
title: string;
type: 'overview' | 'chart';
unit?: string;
color?: string;
}
interface Props {
totalPower: number;
fuelReduction: number;
@ -111,20 +191,164 @@ interface Props {
pm25Reduction: number;
noxReduction: number;
so2Reduction: number;
fuelReductionData: ChartDataItem[];
co2ReductionData: ChartDataItem[];
pm25ReductionData: ChartDataItem[];
noxReductionData: ChartDataItem[];
so2ReductionData: ChartDataItem[];
}
defineProps<Props>()
const props = defineProps<Props>()
const displayMode = ref<'average' | 'magnify'>('average')
const selectedCard = ref<string>('overview')
//
const commonChartData = ref<ChartDataItem[]>([])
//
const cards = ref<CardInfo[]>([
{
id: 'overview',
title: '总览',
type: 'overview'
},
{
id: 'fuel',
title: '减少燃油(吨)',
type: 'chart',
unit: '吨',
color: '#4CAF50' // 绿 -
},
{
id: 'co2',
title: '减少CO₂排放(千克)',
type: 'chart',
unit: '千克',
color: '#2E7D32' // 绿/绿 - 绿
},
{
id: 'pm25',
title: '减少PM2.5排放(千克)',
type: 'chart',
unit: '千克',
color: '#9E9E9E' // -
},
{
id: 'nox',
title: '减少NOₓ排放(千克)',
type: 'chart',
unit: '千克',
color: '#FF9800' // -
},
{
id: 'so2',
title: '减少SO₂排放(千克)',
type: 'chart',
unit: '千克',
color: '#FFEB3B' // -
}
])
const handleSelectCard = (cardId: string) => {
//
if (displayMode.value === 'magnify' && selectedCard.value === cardId) {
displayMode.value = 'average'
} else {
//
displayMode.value = 'magnify'
selectedCard.value = cardId
}
}
//
const generateInitialData = () => {
const now = new Date()
const data: ChartDataItem[] = []
// 510
for (let i = 29; i >= 0; i--) {
const time = new Date(now.getTime() - i * 10000)
data.push({
name: time.toLocaleTimeString(),
value: Math.floor(Math.random() * 100) + 10
})
}
return data
}
//
let timer: number | null = null
//
const appendNewData = () => {
const now = new Date()
commonChartData.value.push({
name: now.toLocaleTimeString(),
value: Math.floor(Math.random() * 100) + 10
})
}
//
onMounted(() => {
commonChartData.value = generateInitialData()
timer = window.setInterval(appendNewData, 1000)
})
//
onBeforeUnmount(() => {
if (timer) {
window.clearInterval(timer)
}
})
</script>
<style scoped>
<style lang="scss" scoped>
.shore-power-usage {
display: flex;
height: 100%;
gap: 10px;
}
.magnify {
width: 100%;
padding: 12px;
height: calc(100vh - 72px);
position: absolute;
top: 72px;
left: 0px;
display: flex;
flex-direction: column;
/* background-color: red; */
gap: 8px;
overflow-y: auto;
.big {
height: 70%;
.card {
height: 100%;
}
.overview-value {
font-size: 32px;
}
.overview-label {
font-size: 24px;
}
}
.one-row {
height: 30%;
display: flex;
gap: 8px;
.card {
flex: 1;
}
}
}
.card {
padding: 6px;
cursor: pointer;
}
.right {
/* gap: 0 */
}
</style>

163
public/map/components/cesiumMap.vue

@ -3,7 +3,7 @@
<div ref="cesiumContainerRef" class="cesium-container"></div>
<div class="btn-group">
<!-- <el-button class="get-view-btn" size="small" type="primary" @click="getCurrentViewInfo"
style="margin-right: 10px;">获取当前视角</el-button> -->
style="margin-right: 10px;">获取当前视角</el-button> -->
<!-- <el-button class="view-btn" size="small" type="success" @click="switchView('overview')"
style="margin-right: 10px;">概览视角</el-button> -->
<!-- <el-button class="view-btn" size="small" type="warning" @click="switchView('view1')"
@ -40,6 +40,14 @@ const presetViews = {
}
};
//
let onElectricalBoxClick = null;
//
const setElectricalBoxClickCallback = (callback) => {
onElectricalBoxClick = callback;
};
const createView = (obj) => {
return {
destination: Cesium.Cartesian3.fromDegrees(obj.longitude, obj.latitude, obj.height),
@ -62,7 +70,7 @@ onMounted(async () => {
viewer = new Cesium.Viewer(cesiumContainerRef.value, {
// ****
imageryProvider: false,
// infoBox: true,
// **使 Cesium Ion**
terrainProvider: new Cesium.EllipsoidTerrainProvider(),
// sceneMode: Cesium.SceneMode.COLUMBUS_VIEW,
@ -104,7 +112,21 @@ onMounted(async () => {
//
viewer.clock.shouldAnimate = true;
viewer.scene.screenSpaceCameraController.minimumZoomDistance = 200;
//
const controller = viewer.scene.screenSpaceCameraController;
//
// 500,000.0 = 500
const MAX_DISTANCE = 500000.0;
//
controller.enableZoom = true;
controller.maximumZoomDistance = MAX_DISTANCE;
//
// 1000.0
controller.minimumZoomDistance = 1000;
// viewer.scene.screenSpaceCameraController.minimumZoomDistance = 200;
// 2:
const gaodeImage = new Cesium.UrlTemplateImageryProvider({
url: "https://webst0{s}.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}",
@ -195,8 +217,8 @@ onMounted(async () => {
dataWithModelsArray.push(itemWithModel); //
return;
}
if (dataObj.icon === 'ship_green') {
console.log('dataObj.icon', dataObj.icon)
if (dataObj.icon === 'ship_green' || dataObj.icon === 'ship_red') {
const itemShipInfo = shipData.find(shipItem => (shipItem.shorePower.id === item.parentId) && item.type === 5)
const position = Cesium.Cartesian3.fromDegrees(wgsLon, wgsLat, 15);
const statusPosition = Cesium.Cartesian3.fromDegrees(wgsLon, wgsLat, 1);
@ -233,8 +255,8 @@ onMounted(async () => {
position: labelPosition, // 10
label: {
text: itemShipInfo.shipBasicInfo.name || `Marker-${item.id || index}`,
font: '12px sans-serif',
fillColor: Cesium.Color.WHITE,
font: '16px sans-serif',
fillColor: Cesium.Color.LIME,
outlineColor: Cesium.Color.BLACK,
outlineWidth: 2,
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
@ -242,6 +264,7 @@ onMounted(async () => {
disableDepthTestDistance: Number.POSITIVE_INFINITY //
}
});
// if (dataObj.icon === 'ship_red') {
const overlayBillboard = viewer.entities.add({
position: statusPosition,
billboard: {
@ -252,11 +275,13 @@ onMounted(async () => {
scale: new Cesium.CallbackProperty(function (time, result) {
// t
const t = Cesium.JulianDate.toDate(time).getTime() / 1000;
const pulse = 0.6 + Math.sin(t * 4) * 0.2; // (0.6, ±0.2)
const pulse = 0.4 + Math.sin(t * 4) * 0.1; // (0.6, ±0.2)
return pulse;
}, false)
}
});
// }
}
if (dataObj.type === 'text') {
const labelPosition = Cesium.Cartesian3.fromDegrees(wgsLon, wgsLat, 35);
@ -264,7 +289,7 @@ onMounted(async () => {
position: labelPosition, // 10
label: {
text: dataObj.name || `Marker-${item.id || index}`,
font: '20px sans-serif',
font: '12px sans-serif',
fillColor: Cesium.Color.WHITE,
outlineColor: Cesium.Color.BLACK,
outlineWidth: 2,
@ -279,8 +304,13 @@ onMounted(async () => {
const labelPosition = Cesium.Cartesian3.fromDegrees(wgsLon, wgsLat, 5);
//
const electricalBoxModel = viewer.entities.add({
name: 'Electrical Box Model',
name: '岸电箱',
position: position,
//
properties: {
modelType: 'electrical_box',
data: itemWithModel
},
orientation: Cesium.Transforms.headingPitchRollQuaternion(
position,
new Cesium.HeadingPitchRoll(
@ -291,13 +321,13 @@ onMounted(async () => {
),
model: {
uri: '/model/electrical_box.glb',
scale: 1, //
scale: 1.5, //
// minimumPixelSize: 10, // 50
// 使
enableDepthTest: true,
//
backFaceCulling: true
}
},
});
//
@ -305,7 +335,12 @@ onMounted(async () => {
itemWithModel.modelType = 'electrical_box';
viewer.entities.add({
// pickable: false,
position: labelPosition, // 10
properties: {
modelType: 'electrical_box',
data: itemWithModel
},
label: {
text: '岸电箱' || `Marker-${item.id || index}`,
font: '10px sans-serif',
@ -317,6 +352,27 @@ onMounted(async () => {
disableDepthTestDistance: Number.POSITIVE_INFINITY //
}
});
const overlayBillboard = viewer.entities.add({
position: position,
// pickable: false,
properties: {
modelType: 'electrical_box',
data: itemWithModel
},
billboard: {
image: '/img/故障.png',
horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
verticalOrigin: Cesium.VerticalOrigin.CENTER,
disableDepthTestDistance: Number.POSITIVE_INFINITY,
scale: new Cesium.CallbackProperty(function (time, result) {
// t
const t = Cesium.JulianDate.toDate(time).getTime() / 1000;
const pulse = 0.2 + Math.sin(t * 4) * 0.1; // (0.6, ±0.2)
return pulse;
}, false)
}
});
}
//
@ -401,6 +457,28 @@ onMounted(async () => {
handler.setInputAction(function (movement) {
console.log('地图点击事件触发');
//
const pickedObject = viewer.scene.pick(movement.position);
if (pickedObject && pickedObject.id && pickedObject.id.properties) {
//
const properties = pickedObject.id.properties;
if (properties.modelType && properties.modelType.getValue() === 'electrical_box') {
//
const electricalBoxData = properties.data.getValue();
console.log('岸电箱模型被点击:', electricalBoxData);
//
if (typeof onElectricalBoxClick === 'function') {
onElectricalBoxClick(electricalBoxData);
}
//
return;
}
}
//
//
const ray = viewer.camera.getPickRay(movement.position);
@ -461,12 +539,58 @@ const switchView = (viewName) => {
}
};
//
const switchModelView = (view) => {
const modelInstance = toRaw(view);
console.log(modelInstance)
if (viewer && view) {
viewer.flyTo(modelInstance);
/**
* 切换到指定模型视图并允许设置自定义的飞离距离
* @param {Cesium.Entity} view - 要飞向的 Entity 实例
* @param {number} [extraDistance=15000.0] - 额外的飞离距离 ()
* @param {number} [estimatedRadius=500.0] - 用于计算视角范围的估计模型半径 ()
*/
const switchModelView = (view, extraDistance = 1000, estimatedRadius = 500.0) => {
const entityInstance = toRaw(view);
console.log('目标模型实体:', entityInstance);
if (viewer && entityInstance && entityInstance.position) {
// 1.
// 使 getValue(viewer.clock.currentTime)
const position = entityInstance.position.getValue(viewer.clock.currentTime);
if (!position) {
console.warn("无法获取模型位置,执行默认 flyTo。");
viewer.flyTo(entityInstance, { duration: 3.0 });
return;
}
// 2. ( Entity readyPromise boundingSphere )
// 使
const manualBoundingSphere = new Cesium.BoundingSphere(
position,
estimatedRadius // 使
);
// 3. (Heading, Pitch, Range)
const cameraOffset = new Cesium.HeadingPitchRange(
Cesium.Math.toRadians(0.0), // Heading (), 0
Cesium.Math.toRadians(-45.0), // Pitch (), -45
manualBoundingSphere.radius + extraDistance // Range () = +
);
// 4.
viewer.camera.flyToBoundingSphere(
manualBoundingSphere,
{
offset: cameraOffset,
duration: 3.0 //
}
);
console.log(`飞往模型中心,距离模型中心 ${(manualBoundingSphere.radius + extraDistance) / 1000} 公里。`);
} else if (viewer && entityInstance) {
// fallback
viewer.flyTo(entityInstance, { duration: 3.0 });
console.warn("模型位置属性无效,执行默认 flyTo。");
} else {
console.error("Cesium Viewer 或模型实例无效。");
}
};
@ -497,7 +621,8 @@ const getCurrentViewInfo = () => {
defineExpose({
switchView,
dataWithModels,
switchModelView
switchModelView,
setElectricalBoxClickCallback
});
onBeforeUnmount(() => {

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

@ -16,7 +16,7 @@ interface Props {
const props = withDefaults(defineProps<Props>(), {
chartData: () => [],
title: '',
color: '#1296db'
color: '#1296DB' // 使
})
//
@ -37,7 +37,7 @@ const updateChart = () => {
const option = {
title: {
text: props.title,
// text: props.title,
textStyle: {
color: '#fff',
fontSize: 14
@ -59,7 +59,7 @@ const updateChart = () => {
grid: {
left: '1%',
right: '1%',
top: '10%',
top: '5%',
bottom: '1%',
containLabel: true
},
@ -119,11 +119,11 @@ const updateChart = () => {
colorStops: [
{
offset: 0,
color: props.color
color: props.color // 使
},
{
offset: 1,
color: props.color + '80'
color: props.color + '80' //
}
]
},
@ -131,7 +131,7 @@ const updateChart = () => {
},
emphasis: {
itemStyle: {
color: props.color
color: props.color // 使
}
}
}
@ -159,7 +159,9 @@ const handleResize = () => {
//
onMounted(() => {
initChart()
setTimeout(() => {
initChart()
}, 200)
window.addEventListener('resize', handleResize)
})
@ -177,6 +179,6 @@ onBeforeUnmount(() => {
.bar-chart-minute-container {
width: 100%;
height: 100%;
min-height: 200px;
/* min-height: 200px; */
}
</style>

4
public/map/components/charts/LineChart.vue

@ -42,7 +42,7 @@ const updateChart = () => {
color: '#fff',
fontSize: 14
},
left: 'center'
// left: 'center'
},
tooltip: {
trigger: 'axis',
@ -115,7 +115,7 @@ const updateChart = () => {
grid: {
left: '1%',
right: '1%',
top: '6%',
top: '5%',
bottom: '1%',
containLabel: true
}

79
public/map/components/charts/PieChart.vue

@ -28,10 +28,19 @@ const initChart = () => {
}
}
//
const getTotalValue = () => {
if (!props.chartData) return 0
const total = props.chartData.reduce((sum, item) => sum + item.value, 0)
return Math.round(total * 100) / 100 //
}
//
const updateChart = () => {
if (!chartInstance || !props.chartData) return
const totalValue = getTotalValue()
const option = {
title: {
// text: props.title,
@ -45,10 +54,10 @@ const updateChart = () => {
},
legend: {
show: true,
type: 'scroll', // 👈
orient: 'vertical', // 👈
right: 10,
top: 'center',
// type: 'scroll', // 👈
orient: 'horizontal', // 👈
top: 10, // 👈
left: 'center', // 👈
textStyle: {
color: '#FFF'
}
@ -57,30 +66,78 @@ const updateChart = () => {
{
name: props.title,
type: 'pie',
radius: ['30%', '50%'],
center: ['35%', '50%'], // 👈
radius: ['35%', '55%'], //
center: ['50%', '40%'], // 👈
padAngle: 5, //
avoidLabelOverlap: true,
itemStyle: {
borderRadius: 0,
borderColor: '#fff',
borderWidth: 1
borderColor: 'transparent', //
borderWidth: 0 // 0
},
label: {
show: true,
formatter: '{b}\n{d}%',
fontSize: 10
formatter: (params) => {
return params.name + '\n' + params.value.toFixed(2)
},
fontSize: 16,
color: '#FFFFFF', //
textBorderColor: 'transparent', //
textBorderWidth: 0
},
emphasis: {
label: {
show: true,
formatter: (params) => {
return params.name + '\n' + params.value.toFixed(2)
},
fontSize: 14,
fontWeight: 'bold'
fontWeight: 'bold',
color: '#FFFFFF', //
textBorderColor: 'transparent', //
textBorderWidth: 0
}
},
labelLine: {
show: true
},
data: props.chartData
},
{
name: '总量',
type: 'pie',
radius: ['0%', '35%'], //
center: ['50%', '40%'],
hoverAnimation: false, //
label: {
show: true,
position: 'center',
formatter: [`{title|总数}`, `{value|${totalValue.toFixed(2)}}`].join('\n'),
rich: {
title: {
color: '#FFF',
fontSize: 14,
lineHeight: 20
},
value: {
color: '#FFF',
fontSize: 20,
fontWeight: 'bold',
lineHeight: 30
}
}
},
labelLine: {
show: false
},
data: [
{
value: totalValue,
itemStyle: {
color: 'rgba(0, 0, 0, 0)' //
}
}
]
}
]
}

381
public/map/index.vue

@ -5,7 +5,7 @@
<div class="head-title">
<span>曹妃甸港区船舶岸电监管平台</span>
</div>
<el-button class="view-btn" size="small" type="success" @click="mapComponentRef.switchView('overview')"
<el-button class="view-btn" size="small" type="success" @click="handleOverviewClick"
style="margin-right: 10px;">概览视角</el-button>
<div class="head-group">
@ -19,23 +19,18 @@
<!-- 港区概览 -->
<template v-if="activeHeadGroup === 0">
<PortOverview :ship-status-data="shipStatusData" :selected-ship="selectedShip" @select-ship="handleSwitch"
:get-status-class="getStatusClass" :get-shore-power-status-text="getShorePowerStatusText" />
<PortOverview :ship-status-data="shipStatusData" :selected-ship="selectedShip" @item-click="handleSwitch" />
</template>
<!-- 港口岸电使用情况 -->
<template v-if="activeHeadGroup === 1">
<ShorePowerUsage :total-power="totalPower" :fuel-reduction="fuelReduction" :co2-reduction="co2Reduction"
:pm25-reduction="pm25Reduction" :nox-reduction="noxReduction" :so2-reduction="so2Reduction"
:fuel-reduction-data="fuelReductionData" :co2-reduction-data="co2ReductionData"
:pm25-reduction-data="pm25ReductionData" :nox-reduction-data="noxReductionData"
:so2-reduction-data="so2ReductionData" />
:pm25-reduction="pm25Reduction" :nox-reduction="noxReduction" :so2-reduction="so2Reduction" />
</template>
<!-- 港口企业岸电使用 -->
<template v-if="activeHeadGroup === 2">
<CompanyShorePower :company-comparison-data="companyComparisonData" :selected-company="selectedCompany"
:pie-chart-data="pieChartData" @update:selected-company="selectedCompany = $event" />
<CompanyShorePower :companyComparisonData="companyComparisonData" />
</template>
<!-- 船舶岸电使用情况 -->
@ -44,7 +39,58 @@
:no-shore-power-ships="noShorePowerShips" :ship-data="shipData" />
</template>
<template v-if="activeHeadGroup === 4">
<div class="right" style="width: 500px;">
<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>
<el-button class="close-btn" size="small" @click="closeElectricalBoxInfo"
style="margin-left: auto;">×</el-button>
</div>
<div class="card-content">
<div class="ship-detail">
<div class="detail-item">
<span class="label">名称:</span>
<span class="value">{{ selectedElectricalBox?.name || '无' }}</span>
</div>
<div class="detail-item">
<span class="label">位置:</span>
<span class="value">{{ selectedElectricalBox?.location || '曹妃甸港区华能码头' }}</span>
</div>
<div class="detail-item">
<span class="label">状态:</span>
<span class="value">{{ selectedElectricalBox?.status || '在线' }}</span>
</div>
<!-- 当前使用数据 -->
<div class="section-title">当前使用数据</div>
<div class="detail-item">
<span class="label">当前使用功率:</span>
<span class="value">{{ selectedElectricalBox?.currentPower || '120' }} kW</span>
</div>
<div class="detail-item">
<span class="label">当前使用电量:</span>
<span class="value">{{ selectedElectricalBox?.currentEnergy || '45.5' }} kWh</span>
</div>
<!-- 累计历史数据 -->
<div class="section-title">累计历史数据</div>
<div class="detail-item">
<span class="label">累计历史用电:</span>
<span class="value">{{ selectedElectricalBox?.totalEnergy || '1250.8' }} kWh</span>
</div>
<div class="detail-item">
<span class="label">累计服务次数:</span>
<span class="value">{{ selectedElectricalBox?.serviceCount || '86' }} </span>
</div>
</div>
</div>
</div>
</div>
</template>
</div>
</template>
@ -73,7 +119,8 @@ const activeHeadGroup = ref<number>(-1)
interface PublicMapComponentsType {
switchView: (viewName: string) => void
dataWithModels: any[]
switchModelView: () => void
switchModelView: (data: any) => void
setElectricalBoxClickCallback: (callback: (data: any) => void) => void
}
const mapComponentRef = ref<PublicMapComponentsType | null>(null)
@ -94,17 +141,17 @@ const selectHeadGroup = async (value: number) => {
const currentTime = ref<string>(dayjs().format('YYYY-MM-DD HH:mm:ss'))
//
const totalPower = ref<string>('857525.45')
const fuelReduction = ref<string>('108.04')
const co2Reduction = ref<string>('574542.06')
const pm25Reduction = ref<string>('1251.99')
const noxReduction = ref<string>('15521.21')
const so2Reduction = ref<string>('9004.03')
const totalPower = ref<number>(857525.45)
const fuelReduction = ref<number>(108.04)
const co2Reduction = ref<number>(574542.06)
const pm25Reduction = ref<number>(1251.99)
const noxReduction = ref<number>(15521.21)
const so2Reduction = ref<number>(9004.03)
// 使
const berthingShips = ref<string>('13')
const shorePowerShips = ref<string>('3')
const noShorePowerShips = ref<string>('10')
const berthingShips = ref<number>(13)
const shorePowerShips = ref<number>(3)
const noShorePowerShips = ref<number>(10)
//
const shipData = ref([
@ -152,35 +199,44 @@ const updateTime = () => {
}
const selectedShip = ref(null)
const selectedElectricalBox = ref(null)
/* 回到初始视角 */
const handleOverviewClick = () => {
mapComponentRef.value?.switchView('overview')
}
/* 切换至模型视角 */
const handleSwitch = (item) => {
console.log(item)
selectedShip.value = item
mapComponentRef.value?.switchModelView(item.modelInstance)
}
const shorePowerStatusMap = {
1: '待靠泊',
2: '靠泊中',
3: '岸电接入中',
4: '用电中',
5: '岸电卸载中',
6: '岸电卸载完成',
7: '离泊',
9: '未使用岸电'
}
//
const handleElectricalBoxClick = (data) => {
activeHeadGroup.value = 4
console.log('岸电箱被点击:', data);
//
selectedElectricalBox.value = data;
//
};
//
const getShorePowerStatusText = (status: number | string) => {
const statusNum = Number(status);
console.log(status)
return shorePowerStatusMap[statusNum] || status;
}
//
const closeElectricalBoxInfo = () => {
activeHeadGroup.value = -1;
selectedElectricalBox.value = null;
};
//
let timer: NodeJS.Timeout
onMounted(() => {
timer = setInterval(updateTime, 1000)
//
if (mapComponentRef.value) {
mapComponentRef.value.setElectricalBoxClickCallback(handleElectricalBoxClick);
}
})
//
@ -348,102 +404,6 @@ watch(
{ deep: true }
)
// 线
const lineChartData = ref([
{ name: '1月', value: 120 },
{ name: '2月', value: 132 },
{ name: '3月', value: 101 },
{ name: '4月', value: 134 },
{ name: '5月', value: 90 },
{ name: '6月', value: 230 },
{ name: '7月', value: 210 },
{ name: '8月', value: 150 },
{ name: '9月', value: 180 },
{ name: '10月', value: 200 },
{ name: '11月', value: 190 },
{ name: '12月', value: 220 }
])
//
const fuelReductionData = ref([
{ name: '1月', value: 80 },
{ name: '2月', value: 85 },
{ name: '3月', value: 70 },
{ name: '4月', value: 90 },
{ name: '5月', value: 60 },
{ name: '6月', value: 150 },
{ name: '7月', value: 140 },
{ name: '8月', value: 100 },
{ name: '9月', value: 120 },
{ name: '10月', value: 130 },
{ name: '11月', value: 125 },
{ name: '12月', value: 145 }
])
//
const co2ReductionData = ref([
{ name: '1月', value: 200 },
{ name: '2月', value: 220 },
{ name: '3月', value: 180 },
{ name: '4月', value: 240 },
{ name: '5月', value: 160 },
{ name: '6月', value: 380 },
{ name: '7月', value: 350 },
{ name: '8月', value: 260 },
{ name: '9月', value: 300 },
{ name: '10月', value: 320 },
{ name: '11月', value: 310 },
{ name: '12月', value: 360 }
])
// PM2.5
const pm25ReductionData = ref([
{ name: '1月', value: 15 },
{ name: '2月', value: 18 },
{ name: '3月', value: 12 },
{ name: '4月', value: 20 },
{ name: '5月', value: 10 },
{ name: '6月', value: 35 },
{ name: '7月', value: 32 },
{ name: '8月', value: 22 },
{ name: '9月', value: 28 },
{ name: '10月', value: 30 },
{ name: '11月', value: 29 },
{ name: '12月', value: 33 }
])
//
const noxReductionData = ref([
{ name: '1月', value: 25 },
{ name: '2月', value: 28 },
{ name: '3月', value: 22 },
{ name: '4月', value: 30 },
{ name: '5月', value: 18 },
{ name: '6月', value: 45 },
{ name: '7月', value: 42 },
{ name: '8月', value: 32 },
{ name: '9月', value: 38 },
{ name: '10月', value: 40 },
{ name: '11月', value: 39 },
{ name: '12月', value: 43 }
])
//
const so2ReductionData = ref([
{ name: '1月', value: 20 },
{ name: '2月', value: 22 },
{ name: '3月', value: 18 },
{ name: '4月', value: 25 },
{ name: '5月', value: 15 },
{ name: '6月', value: 38 },
{ name: '7月', value: 35 },
{ name: '8月', value: 28 },
{ name: '9月', value: 32 },
{ name: '10月', value: 34 },
{ name: '11月', value: 33 },
{ name: '12月', value: 37 }
])
// 使
const companyComparisonData = ref([
{
@ -493,16 +453,6 @@ const selectedCompany = ref('华能码头')
//
const pieChartData = ref<Array<{ name: string; value: number }>>([])
//
const handleCompanyChange = (companyName: string) => {
const selectedCompanyData = companyComparisonData.value.find(company => company.name === companyName)
if (selectedCompanyData && selectedCompanyData.children) {
pieChartData.value = selectedCompanyData.children
} else {
pieChartData.value = []
}
}
//
onMounted(async () => {
//
@ -561,29 +511,6 @@ const shipStatusData = computed(() => {
})
})
//
const getStatusClass = (status: string) => {
switch (status) {
case '正常':
return 'status-normal'
case '空闲':
return 'status-idle'
case '故障':
return 'status-fault'
case '异常':
return 'status-abnormal'
case '维修中':
return 'status-maintenance'
case '岸电使用中':
return 'status-shorepower'
// case '':
// return 'status-fault'
default:
return 'status-default'
}
}
</script>
<style lang="scss">
.cesium-viewer-bottom {
@ -699,36 +626,6 @@ const getStatusClass = (status: string) => {
width: 100%;
// padding: 10px;
}
.overview-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px;
padding: 2px;
}
.overview-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 4px;
background-color: rgba(0, 0, 0, 0.2);
border-radius: 8px;
}
.overview-value {
font-size: 18px;
font-weight: bold;
color: #1296db;
text-shadow: 0 0 5px rgba(0, 0, 0, 0.5);
margin-bottom: 5px;
}
.overview-label {
text-align: center;
font-size: 12px;
color: #ccc;
}
}
/* 船舶数据表格样式 */
@ -805,23 +702,6 @@ const getStatusClass = (status: string) => {
width: 35%;
}
/* 状态样式 */
.status-using {
color: #00ff00;
}
.status-no-equipment {
color: #ff6600;
}
.status-damaged {
color: #ff3300;
}
.status-cable {
color: #ffcc00;
}
.head {
display: flex;
justify-content: space-between;
@ -1111,4 +991,83 @@ const getStatusClass = (status: string) => {
opacity: 0;
transform: translateX(20px);
}
.overview-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px;
padding: 2px;
height: 100%;
}
.overview-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 4px;
background-color: rgba(0, 0, 0, 0.2);
border-radius: 8px;
}
.overview-value {
font-size: 18px;
font-weight: bold;
color: #1296db;
text-shadow: 0 0 5px rgba(0, 0, 0, 0.5);
margin-bottom: 5px;
}
.overview-label {
text-align: center;
font-size: 12px;
color: #ccc;
}
/* 状态样式 */
.status-using {
color: #00ff00;
}
.status-no-equipment {
color: #ff6600;
}
.status-damaged {
color: #ff3300;
}
.status-cable {
color: #ffcc00;
}
/* 岸电箱详情样式 */
.section-title {
font-size: 18px;
font-weight: bold;
color: #1296db;
margin: 15px 0 8px 0;
padding-bottom: 5px;
border-bottom: 1px solid rgba(18, 150, 219, 0.3);
}
.detail-item {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
padding: 8px 12px;
border-radius: 6px;
background-color: rgba(0, 0, 0, 0.15);
}
.label {
color: #ddd;
font-size: 16px;
}
.value {
color: #fff;
font-size: 16px;
font-weight: 600;
}
</style>

Loading…
Cancel
Save