13 changed files with 799 additions and 572 deletions
@ -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> |
|||
@ -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> |
|||
@ -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> |
|||
@ -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,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> |
|||
@ -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> |
|||
Loading…
Reference in new issue