33 changed files with 7875 additions and 1478 deletions
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
Binary file not shown.
@ -0,0 +1,256 @@ |
|||||
|
<template> |
||||
|
<div class="company-shore-power"> |
||||
|
<div class="left" style="width: 1000px;"> |
||||
|
<!-- 码头信息卡片 --> |
||||
|
<div class="card digital-twin-card--deep-blue"> |
||||
|
<div class="card-title" 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> |
||||
|
<!-- <div> |
||||
|
<el-date-picker type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" |
||||
|
format="YYYY-MM-DD" value-format="YYYY-MM-DD" class="date-range-picker" /> |
||||
|
</div> --> |
||||
|
|
||||
|
</div> |
||||
|
|
||||
|
<div class="card-content"> |
||||
|
<div v-for="company in companyComparisonData" :key="company.name" class="company-section"> |
||||
|
<div class="company-header"> |
||||
|
<div class="company-name">{{ company.name }}</div> |
||||
|
<div class="company-total">总量: {{ calculateTotal(company.children).toFixed(2) }}</div> |
||||
|
</div> |
||||
|
<div class="overview-grid"> |
||||
|
<div v-for="(berth, index) in (company.children || [])" :key="index" class="overview-item" |
||||
|
@click="showShorePowerHistory(berth)"> |
||||
|
<div class="overview-value">{{ berth.measureValue?.toFixed(2) |
||||
|
|| 0 }}</div> |
||||
|
|
||||
|
<div class="overview-label">{{ berth.name }}</div> |
||||
|
|
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<ship-history-dialog v-if="historyVisible.visible" v-model="historyVisible.visible" |
||||
|
:ship-param="historyVisible.searchParams" :realtime-device-data="realtimeDeviceData" /> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
import { ref } from 'vue' |
||||
|
import { MapApi } from "@/api/shorepower/map"; |
||||
|
import ShipHistoryDialog from './ShipHistoryDialog.vue'; |
||||
|
import { RealtimeDeviceData } from '@/types/shorepower'; |
||||
|
// 定义组件属性 |
||||
|
interface ChartDataItem { |
||||
|
name: string; |
||||
|
value?: number; |
||||
|
measureValue?: number; |
||||
|
children?: ChartDataItem[]; |
||||
|
} |
||||
|
|
||||
|
const historyVisible = ref({ |
||||
|
visible: false, |
||||
|
searchParams: { |
||||
|
shipId: 0 as number | null, |
||||
|
ids: [] as number[] | null, |
||||
|
type: 1 as number |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
// 计算总量的函数 |
||||
|
const calculateTotal = (children?: ChartDataItem[]) => { |
||||
|
if (!children || children.length === 0) return 0; |
||||
|
return children.reduce((total, item) => { |
||||
|
return total + (item.measureValue || 0); |
||||
|
}, 0); |
||||
|
}; |
||||
|
|
||||
|
// 显示岸电箱历史记录 |
||||
|
const showShorePowerHistory = (berth: ChartDataItem) => { |
||||
|
console.log(berth) |
||||
|
// console.log('shorepower', shorepower) |
||||
|
// currentShorePower.value = shorepower |
||||
|
/* shorePowerHistoryVisible.value = { |
||||
|
visible: true, |
||||
|
shorePowerId: shorepower.id || 0 |
||||
|
} */ |
||||
|
historyVisible.value = { |
||||
|
visible: true, |
||||
|
searchParams: { |
||||
|
shipId: null, |
||||
|
ids: [berth.id], |
||||
|
type: 4, |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
interface Props { |
||||
|
companyComparisonData?: ChartDataItem[]; |
||||
|
realtimeDeviceData: RealtimeDeviceData[]; |
||||
|
} |
||||
|
|
||||
|
const props = defineProps<Props>() |
||||
|
const companyComparisonData = ref<ChartDataItem[]>([]) |
||||
|
const handleGetBuildData = async () => { |
||||
|
const dockList = await MapApi.getDockIdAndNameList() |
||||
|
const berthList = await MapApi.getBerthIdAndNameList() |
||||
|
const buildData = dockList.map(dock => ({ |
||||
|
...dock, |
||||
|
children: berthList.filter(berth => berth.dockId === dock.id).map(berth => ({ |
||||
|
...berth, |
||||
|
...props.realtimeDeviceData.find(item => (item.id === berth.id) && (item.deviceCode.includes('Kwh'))) |
||||
|
})) |
||||
|
})) |
||||
|
console.log('buildData', buildData) |
||||
|
console.log('props.realtimeDeviceData', props.realtimeDeviceData) |
||||
|
companyComparisonData.value = buildData |
||||
|
/* MapApi.getBerthIdAndNameList().then(res => { |
||||
|
console.log(res) |
||||
|
const buildData = res.map(item => ({ |
||||
|
...item, |
||||
|
children: props.realtimeDeviceData.filter(item => item.id === item.id) |
||||
|
})) |
||||
|
|
||||
|
}) */ |
||||
|
} |
||||
|
watch(() => props.realtimeDeviceData, (newVal) => { |
||||
|
handleGetBuildData() |
||||
|
}) |
||||
|
onMounted(() => { |
||||
|
// console.log(props.realtimeDeviceData) |
||||
|
handleGetBuildData() |
||||
|
|
||||
|
}) |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.company-shore-power { |
||||
|
display: flex; |
||||
|
height: 100%; |
||||
|
gap: 10px; |
||||
|
} |
||||
|
|
||||
|
.left { |
||||
|
/* display: flex; |
||||
|
flex-direction: column; |
||||
|
height: 100%; |
||||
|
gap: 10px; */ |
||||
|
} |
||||
|
|
||||
|
.card { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
height: 100%; |
||||
|
} |
||||
|
|
||||
|
.card-content { |
||||
|
flex: 1; |
||||
|
height: 100%; |
||||
|
min-height: 0; |
||||
|
padding-bottom: 50px; |
||||
|
overflow-y: auto; |
||||
|
} |
||||
|
|
||||
|
/* 总览网格样式 */ |
||||
|
.overview-grid { |
||||
|
display: grid; |
||||
|
grid-template-columns: 1fr 1fr 1fr 1fr; |
||||
|
gap: 8px; |
||||
|
padding: 4px; |
||||
|
height: 100%; |
||||
|
} |
||||
|
|
||||
|
.overview-item { |
||||
|
cursor: pointer; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
padding: 4px; |
||||
|
background-color: rgba(0, 0, 0, 0.2); |
||||
|
border-radius: 6px; |
||||
|
} |
||||
|
|
||||
|
.overview-label { |
||||
|
text-align: center; |
||||
|
font-size: 24px; |
||||
|
color: #ccc; |
||||
|
margin-bottom: 2px; |
||||
|
} |
||||
|
|
||||
|
.overview-value { |
||||
|
font-size: 36px; |
||||
|
font-weight: bold; |
||||
|
color: #1296db; |
||||
|
text-shadow: 0 0 5px rgba(0, 0, 0, 0.5); |
||||
|
margin-bottom: 2px; |
||||
|
} |
||||
|
|
||||
|
.overview-text { |
||||
|
font-size: 10px; |
||||
|
color: #999; |
||||
|
text-align: center; |
||||
|
} |
||||
|
|
||||
|
/* 企业区块样式 */ |
||||
|
.company-section { |
||||
|
margin-bottom: 16px; |
||||
|
padding-bottom: 16px; |
||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1); |
||||
|
} |
||||
|
|
||||
|
.company-section:last-child { |
||||
|
border-bottom: none; |
||||
|
margin-bottom: 0; |
||||
|
padding-bottom: 0; |
||||
|
} |
||||
|
|
||||
|
/* 企业头部样式 */ |
||||
|
.company-header { |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
margin-bottom: 8px; |
||||
|
padding: 0 4px; |
||||
|
} |
||||
|
|
||||
|
.company-name { |
||||
|
font-size: 24px; |
||||
|
font-weight: bold; |
||||
|
color: #fff; |
||||
|
} |
||||
|
|
||||
|
.company-total { |
||||
|
font-size: 24px; |
||||
|
font-weight: bold; |
||||
|
color: #1296db; |
||||
|
background-color: rgba(18, 150, 219, 0.1); |
||||
|
padding: 2px 8px; |
||||
|
border-radius: 4px; |
||||
|
} |
||||
|
|
||||
|
/* 自定义滚动条样式 */ |
||||
|
.card-content::-webkit-scrollbar { |
||||
|
width: 6px; |
||||
|
} |
||||
|
|
||||
|
.card-content::-webkit-scrollbar-track { |
||||
|
background: rgba(255, 255, 255, 0.1); |
||||
|
border-radius: 3px; |
||||
|
} |
||||
|
|
||||
|
.card-content::-webkit-scrollbar-thumb { |
||||
|
background: rgba(17, 138, 237, 0.7); |
||||
|
border-radius: 3px; |
||||
|
} |
||||
|
|
||||
|
.card-content::-webkit-scrollbar-thumb:hover { |
||||
|
background: rgba(17, 138, 237, 1); |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,174 @@ |
|||||
|
<template> |
||||
|
<el-dialog v-model="visible" :title="title" width="1200px" :before-close="handleClose"> |
||||
|
<!-- 搜索和筛选区域 --> |
||||
|
<div class="filter-container"> |
||||
|
<el-form :model="queryParams" label-width="80px" inline> |
||||
|
<el-form-item label="关键字"> |
||||
|
<el-input v-model="queryParams.keyword" placeholder="请输入关键字搜索" clearable @keyup.enter="handleSearch" /> |
||||
|
</el-form-item> |
||||
|
<el-form-item label="时间范围"> |
||||
|
<el-date-picker v-model="dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" |
||||
|
end-placeholder="结束日期" value-format="YYYY-MM-DD" @change="handleDateChange" /> |
||||
|
</el-form-item> |
||||
|
<el-form-item> |
||||
|
<el-button type="primary" @click="handleSearch">搜索</el-button> |
||||
|
<el-button @click="resetQuery">重置</el-button> |
||||
|
</el-form-item> |
||||
|
</el-form> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 数据表格 --> |
||||
|
<el-table :data="tableData" border stripe style="width: 100%" v-loading="loading"> |
||||
|
<el-table-column v-for="column in tableColumns" :key="column.prop" :prop="column.prop" :label="column.label" |
||||
|
:width="column.width" :formatter="column.formatter" /> |
||||
|
</el-table> |
||||
|
|
||||
|
<!-- 分页 --> |
||||
|
<div class="pagination-container"> |
||||
|
<el-pagination v-model:current-page="queryParams.pageNo" v-model:page-size="queryParams.pageSize" |
||||
|
:page-sizes="[10, 20, 50, 100]" :total="total" layout="total, sizes, prev, pager, next, jumper" |
||||
|
@size-change="handleSizeChange" @current-change="handleCurrentChange" /> |
||||
|
</div> |
||||
|
|
||||
|
<template #footer> |
||||
|
<span class="dialog-footer"> |
||||
|
<el-button @click="handleClose">关闭</el-button> |
||||
|
</span> |
||||
|
</template> |
||||
|
</el-dialog> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
import { ref, computed, watch } from 'vue' |
||||
|
import { ElDialog, ElForm, ElFormItem, ElInput, ElDatePicker, ElButton, ElTable, ElTableColumn, ElPagination } from 'element-plus' |
||||
|
|
||||
|
// 定义组件属性 |
||||
|
interface Props { |
||||
|
modelValue: boolean |
||||
|
title?: string |
||||
|
columns: Array<{ |
||||
|
prop: string |
||||
|
label: string |
||||
|
width?: string | number |
||||
|
formatter?: (row: any, column: any, cellValue: any) => string |
||||
|
}> |
||||
|
data: any[] |
||||
|
} |
||||
|
|
||||
|
// 定义事件 |
||||
|
const emit = defineEmits<{ |
||||
|
(e: 'update:modelValue', value: boolean): void |
||||
|
(e: 'search', params: any): void |
||||
|
}>() |
||||
|
|
||||
|
// 属性定义 |
||||
|
const props = withDefaults(defineProps<Props>(), { |
||||
|
modelValue: false, |
||||
|
title: '历史记录', |
||||
|
columns: () => [], |
||||
|
data: () => [] |
||||
|
}) |
||||
|
|
||||
|
// 响应式数据 |
||||
|
const visible = computed({ |
||||
|
get: () => props.modelValue, |
||||
|
set: (val) => emit('update:modelValue', val) |
||||
|
}) |
||||
|
|
||||
|
const queryParams = ref({ |
||||
|
keyword: '', |
||||
|
startTime: '', |
||||
|
endTime: '', |
||||
|
pageNo: 1, |
||||
|
pageSize: 10 |
||||
|
}) |
||||
|
|
||||
|
const dateRange = ref<[string, string]>(['', '']) |
||||
|
const loading = ref(false) |
||||
|
const total = ref(0) |
||||
|
|
||||
|
// 表格数据计算属性 |
||||
|
const tableData = computed(() => { |
||||
|
// 在实际应用中,这里应该是从服务器获取的数据 |
||||
|
// 当前为了演示,我们使用传入的模拟数据 |
||||
|
return props.data.slice( |
||||
|
(queryParams.value.pageNo - 1) * queryParams.value.pageSize, |
||||
|
queryParams.value.pageNo * queryParams.value.pageSize |
||||
|
) |
||||
|
}) |
||||
|
|
||||
|
// 表格列 |
||||
|
const tableColumns = computed(() => props.columns) |
||||
|
|
||||
|
// 处理关闭 |
||||
|
const handleClose = () => { |
||||
|
visible.value = false |
||||
|
} |
||||
|
|
||||
|
// 处理搜索 |
||||
|
const handleSearch = () => { |
||||
|
// 发送搜索事件给父组件 |
||||
|
emit('search', { ...queryParams.value }) |
||||
|
} |
||||
|
|
||||
|
// 重置查询条件 |
||||
|
const resetQuery = () => { |
||||
|
queryParams.value.keyword = '' |
||||
|
queryParams.value.startTime = '' |
||||
|
queryParams.value.endTime = '' |
||||
|
dateRange.value = ['', ''] |
||||
|
queryParams.value.pageNo = 1 |
||||
|
handleSearch() |
||||
|
} |
||||
|
|
||||
|
// 处理日期变化 |
||||
|
const handleDateChange = (val: [string, string] | null) => { |
||||
|
if (val && val[0] && val[1]) { |
||||
|
queryParams.value.startTime = val[0] |
||||
|
queryParams.value.endTime = val[1] |
||||
|
} else { |
||||
|
queryParams.value.startTime = '' |
||||
|
queryParams.value.endTime = '' |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 处理分页大小变化 |
||||
|
const handleSizeChange = (val: number) => { |
||||
|
queryParams.value.pageSize = val |
||||
|
queryParams.value.pageNo = 1 |
||||
|
handleSearch() |
||||
|
} |
||||
|
|
||||
|
// 处理当前页变化 |
||||
|
const handleCurrentChange = (val: number) => { |
||||
|
queryParams.value.pageNo = val |
||||
|
handleSearch() |
||||
|
} |
||||
|
|
||||
|
// 监听数据变化并更新总数 |
||||
|
watch( |
||||
|
() => props.data, |
||||
|
(newData) => { |
||||
|
total.value = newData.length |
||||
|
}, |
||||
|
{ immediate: true } |
||||
|
) |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.filter-container { |
||||
|
margin-bottom: 20px; |
||||
|
} |
||||
|
|
||||
|
.pagination-container { |
||||
|
margin-top: 20px; |
||||
|
display: flex; |
||||
|
justify-content: flex-end; |
||||
|
} |
||||
|
|
||||
|
.dialog-footer { |
||||
|
display: flex; |
||||
|
justify-content: flex-end; |
||||
|
gap: 10px; |
||||
|
} |
||||
|
</style> |
||||
@ -1,126 +0,0 @@ |
|||||
<template> |
|
||||
<div ref="chartContainer" class="pie-chart-container"></div> |
|
||||
</template> |
|
||||
|
|
||||
<script setup lang="ts"> |
|
||||
import { ref, onMounted, onBeforeUnmount, watch } from 'vue' |
|
||||
import * as echarts from 'echarts' |
|
||||
|
|
||||
// 定义组件props |
|
||||
interface Props { |
|
||||
chartData?: Array<{ name: string; value: number }> |
|
||||
title?: string |
|
||||
} |
|
||||
|
|
||||
const props = withDefaults(defineProps<Props>(), { |
|
||||
chartData: () => [], |
|
||||
title: '' |
|
||||
}) |
|
||||
|
|
||||
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) return |
|
||||
|
|
||||
const option = { |
|
||||
title: { |
|
||||
// text: props.title, |
|
||||
left: 'center', |
|
||||
textStyle: { |
|
||||
color: '#FFF' |
|
||||
} |
|
||||
}, |
|
||||
tooltip: { |
|
||||
trigger: 'item' |
|
||||
}, |
|
||||
legend: { |
|
||||
show: true, |
|
||||
type: 'scroll', // 👈 开启可滚动图例 |
|
||||
orient: 'vertical', // 👈 垂直排列 |
|
||||
right: 10, |
|
||||
top: 'center', |
|
||||
textStyle: { |
|
||||
color: '#FFF' |
|
||||
} |
|
||||
}, |
|
||||
series: [ |
|
||||
{ |
|
||||
name: props.title, |
|
||||
type: 'pie', |
|
||||
radius: ['30%', '50%'], |
|
||||
center: ['35%', '50%'], // 👈 左移饼图避免与图例重叠 |
|
||||
avoidLabelOverlap: true, |
|
||||
itemStyle: { |
|
||||
borderRadius: 0, |
|
||||
borderColor: '#fff', |
|
||||
borderWidth: 1 |
|
||||
}, |
|
||||
label: { |
|
||||
show: true, |
|
||||
formatter: '{b}\n{d}%', |
|
||||
fontSize: 10 |
|
||||
}, |
|
||||
emphasis: { |
|
||||
label: { |
|
||||
show: true, |
|
||||
fontSize: 14, |
|
||||
fontWeight: 'bold' |
|
||||
} |
|
||||
}, |
|
||||
labelLine: { |
|
||||
show: true |
|
||||
}, |
|
||||
data: props.chartData |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
|
|
||||
chartInstance.setOption(option, true) |
|
||||
} |
|
||||
|
|
||||
// 窗口大小改变时重置图表大小 |
|
||||
const handleResize = () => { |
|
||||
if (chartInstance) { |
|
||||
chartInstance.resize() |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// 监听数据变化 |
|
||||
watch( |
|
||||
() => props.chartData, |
|
||||
() => { |
|
||||
updateChart() |
|
||||
}, |
|
||||
{ deep: true } |
|
||||
) |
|
||||
|
|
||||
// 初始化和销毁 |
|
||||
onMounted(() => { |
|
||||
initChart() |
|
||||
window.addEventListener('resize', handleResize) |
|
||||
}) |
|
||||
|
|
||||
onBeforeUnmount(() => { |
|
||||
if (chartInstance) { |
|
||||
chartInstance.dispose() |
|
||||
} |
|
||||
window.removeEventListener('resize', handleResize) |
|
||||
}) |
|
||||
</script> |
|
||||
|
|
||||
<style scoped> |
|
||||
.pie-chart-container { |
|
||||
width: 100%; |
|
||||
height: 100%; |
|
||||
} |
|
||||
</style> |
|
||||
@ -0,0 +1,506 @@ |
|||||
|
<template> |
||||
|
<div class="port-overview"> |
||||
|
<div class="left" style="width:45%;"> |
||||
|
<div style="height: 100%; width: 100%; display: flex; gap: 8px; "> |
||||
|
<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="搜索岸电设备" v-model="storeSearchKeyword" |
||||
|
@input="handlStoreSearch" /> |
||||
|
</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 class="ship-table-column ship-status-header">历史</div> |
||||
|
</div> |
||||
|
<div class="ship-table-body"> |
||||
|
<div v-for="shorepower in filteredShorePowerList" :key="shorepower.id" class="ship-table-row" |
||||
|
@click="handleSelectShorePower(shorepower)"> |
||||
|
<div class="ship-table-column ship-name"> |
||||
|
<div class="ship-icon">⚡</div> |
||||
|
<span class="ship-name-text">{{ shorepower.name }}</span> |
||||
|
</div> |
||||
|
<div class="ship-table-column ship-status"> |
||||
|
<div class="status-tag" :class="getStatusClass(shorepower.storePowerStatus)"> |
||||
|
{{ shorepower.storePowerStatus }} |
||||
|
</div> |
||||
|
<!-- <div class="status-tag" :class="getStatusClass('空闲')"> |
||||
|
空闲 |
||||
|
</div> |
||||
|
<div class="status-tag" :class="getStatusClass('故障')"> |
||||
|
故障 |
||||
|
</div> --> |
||||
|
</div> |
||||
|
<div class="ship-table-column ship-status-header"> |
||||
|
<el-button type="primary" link @click.stop="showShorePowerHistory(shorepower)">查看</el-button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<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="搜索船舶" v-model="searchKeyword" |
||||
|
@input="handleSearch" /> |
||||
|
</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 class="ship-table-column ship-status-header">历史</div> |
||||
|
</div> |
||||
|
<div class="ship-table-body"> |
||||
|
<div v-for="ship in filteredShipStatusData" :key="ship.id" class="ship-table-row" |
||||
|
@click="handleSelectItem(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(ship.shipStatus)"> |
||||
|
{{ ship.shipStatus }} |
||||
|
</div> |
||||
|
<!-- <div class="status-tag" :class="getStatusClass('空闲')"> |
||||
|
空闲 |
||||
|
</div> |
||||
|
<div class="status-tag" :class="getStatusClass('故障')"> |
||||
|
故障 |
||||
|
</div> --> |
||||
|
</div> |
||||
|
<div class="ship-table-column ship-status-header"> |
||||
|
<el-button type="primary" link @click.stop="showShipHistory(ship)">查看</el-button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div v-if="shipSelectedItem || shorepowerSelectedItem" class="right" style="width: 20%"> |
||||
|
<div v-if="shipSelectedItem && show_type === 'ship'" 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">{{ shipSelectedItem.shipBasicInfo.name + '-' + shipSelectedItem.shipBasicInfo.nameEn |
||||
|
}}</span> |
||||
|
</div> |
||||
|
<!-- <div class="detail-item"> |
||||
|
<span class="label">英文船名:</span> |
||||
|
<span class="value">{{ selectedItem.shipBasicInfo.nameEn }}</span> |
||||
|
</div> --> |
||||
|
<!-- <div class="detail-item"> |
||||
|
<span class="label">船舶呼号:</span> |
||||
|
<span class="value">{{ selectedItem.shipBasicInfo.callSign }}</span> |
||||
|
</div> --> |
||||
|
<div class="detail-item"> |
||||
|
<span class="label">长度:</span> |
||||
|
<span class="value">{{ shipSelectedItem.shipBasicInfo.length }} 米</span> |
||||
|
</div> |
||||
|
<div class="detail-item"> |
||||
|
<span class="label">宽度:</span> |
||||
|
<span class="value">{{ shipSelectedItem.shipBasicInfo.width }} 米</span> |
||||
|
</div> |
||||
|
<div class="detail-item"> |
||||
|
<span class="label">吨位:</span> |
||||
|
<span class="value">{{ shipSelectedItem.shipBasicInfo.tonnage }} 吨</span> |
||||
|
</div> |
||||
|
<div class="detail-item"> |
||||
|
<span class="label">满载吃水深度:</span> |
||||
|
<span class="value">{{ shipSelectedItem.shipBasicInfo.fullLoadDraft }} 米</span> |
||||
|
</div> |
||||
|
<div class="detail-item"> |
||||
|
<span class="label">电压:</span> |
||||
|
<span class="value">{{ getValueById(realtimeDeviceData, shipSelectedItem.shorePower.voltageDeviceId, |
||||
|
'measureValue') }}</span> |
||||
|
</div> |
||||
|
<div class="detail-item"> |
||||
|
<span class="label">电流:</span> |
||||
|
<span class="value">{{ getValueById(realtimeDeviceData, shipSelectedItem.shorePower.currentDeviceId, |
||||
|
'measureValue') }}</span> |
||||
|
</div> |
||||
|
<div class="detail-item"> |
||||
|
<span class="label">频率:</span> |
||||
|
<span class="value">{{ getValueById(realtimeDeviceData, shipSelectedItem.shorePower.frequencyDeviceId, |
||||
|
'measureValue') }}</span> |
||||
|
</div> |
||||
|
<div class="detail-item"> |
||||
|
<span class="label">靠泊状态:</span> |
||||
|
<span class="value">{{ getOperationTypeLabel(shipSelectedItem.shorePowerAndShip.status, |
||||
|
SHORE_POWER_STATUS, |
||||
|
) }}</span> |
||||
|
</div> |
||||
|
<div class="detail-item"> |
||||
|
<span class="label">靠泊类型:</span> |
||||
|
<span class="value">{{ getOperationTypeLabel(shipSelectedItem.shorePowerAndShip.type, BERTH_TYPE) |
||||
|
}}</span> |
||||
|
</div> |
||||
|
<div class="detail-item"> |
||||
|
<span class="label">靠泊时间:</span> |
||||
|
<span class="value">{{ formatTimestamp(shipSelectedItem?.usageRecordInfo?.actualBerthTime) }}</span> |
||||
|
</div> |
||||
|
<div class="detail-item"> |
||||
|
<span class="label">当前状态:</span> |
||||
|
<span class="value">{{ shipSelectedItem.shipStatus }}</span> |
||||
|
</div> |
||||
|
<div v-if="shipSelectedItem.applyInfo.reason === 0" class="detail-item"> |
||||
|
<span class="label">岸电使用时长:</span> |
||||
|
<span class="value">{{ showStatus(shipSelectedItem, realtimeDeviceData)?.useTime }}</span> |
||||
|
</div> |
||||
|
<div v-if="shipSelectedItem.applyInfo.reason === 0" class="detail-item"> |
||||
|
<span class="label">岸电使用用量:</span> |
||||
|
<span class="value">{{ showStatus(shipSelectedItem, realtimeDeviceData)?.useValue }}</span> |
||||
|
</div> |
||||
|
<div v-if="shipSelectedItem.applyInfo.reason != 0" class="detail-item"> |
||||
|
<span class="label">未使用岸电原因:</span> |
||||
|
<span class="value">{{ getOperationTypeLabel(shipSelectedItem?.applyInfo?.reason, |
||||
|
UNUSED_SHORE_POWER_REASON) }}</span> |
||||
|
</div> |
||||
|
<div class="detail-item"> |
||||
|
<span class="label">岸电联系人:</span> |
||||
|
<span class="value">{{ shipSelectedItem.shipBasicInfo.shorePowerContact }}</span> |
||||
|
</div> |
||||
|
<div class="detail-item"> |
||||
|
<span class="label">联系方式:</span> |
||||
|
<span class="value">{{ shipSelectedItem.shipBasicInfo.shorePowerContactPhone }}</span> |
||||
|
</div> |
||||
|
<!-- <div class="detail-item"> |
||||
|
<span class="label">航运单位:</span> |
||||
|
<span class="value">{{ shipSelectedItem.shipBasicInfo.shippingCompany }}</span> |
||||
|
</div> |
||||
|
<div class="detail-item"> |
||||
|
<span class="label">岸电联系人:</span> |
||||
|
<span class="value">{{ shipSelectedItem.shipBasicInfo.shorePowerContact }}</span> |
||||
|
</div> |
||||
|
<div class="detail-item"> |
||||
|
<span class="label">联系方式:</span> |
||||
|
<span class="value">{{ shipSelectedItem.shipBasicInfo.shorePowerContactPhone }}</span> |
||||
|
</div> |
||||
|
<div class="detail-item"> |
||||
|
<span class="label">船检登记号:</span> |
||||
|
<span class="value">{{ shipSelectedItem.shipBasicInfo.inspectionNo }}</span> |
||||
|
</div> |
||||
|
<div class="detail-item"> |
||||
|
<span class="label">创建时间:</span> |
||||
|
<span class="value">{{ new Date(shipSelectedItem.shipBasicInfo.createTime).toLocaleString() }}</span> |
||||
|
</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"> |
||||
|
<div class="ship-detail"> |
||||
|
|
||||
|
</div> |
||||
|
</div> |
||||
|
</div> --> |
||||
|
</div> |
||||
|
<div v-if="shorepowerSelectedItem && show_type === 'shorepower'" 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">{{ shorepowerSelectedItem?.name }}</span> |
||||
|
</div> |
||||
|
<div class="detail-item"> |
||||
|
<span class="label">位置:</span> |
||||
|
<span class="value">{{ shorepowerSelectedItem?.position }}</span> |
||||
|
</div> |
||||
|
<div class="detail-item"> |
||||
|
<span class="label">电压:</span> |
||||
|
<span class="value">{{ shorepowerSelectedItem?.shorePowerEquipmentInfo?.voltage }}</span> |
||||
|
</div> |
||||
|
<div class="detail-item"> |
||||
|
<span class="label">频率:</span> |
||||
|
<span class="value">{{ shorepowerSelectedItem?.shorePowerEquipmentInfo?.frequency }} </span> |
||||
|
</div> |
||||
|
<div class="detail-item"> |
||||
|
<span class="label">容量:</span> |
||||
|
<span class="value">{{ shorepowerSelectedItem?.shorePowerEquipmentInfo?.capacity }}</span> |
||||
|
</div> |
||||
|
<div class="detail-item"> |
||||
|
<span class="label">当前电压:</span> |
||||
|
<span class="value">{{ getValueById(realtimeDeviceData, shorepowerSelectedItem.voltageDeviceId, |
||||
|
'measureValue') }}</span> |
||||
|
</div> |
||||
|
<div class="detail-item"> |
||||
|
<span class="label">当前电流:</span> |
||||
|
<span class="value">{{ getValueById(realtimeDeviceData, shorepowerSelectedItem.currentDeviceId, |
||||
|
'measureValue') }}</span> |
||||
|
</div> |
||||
|
<div class="detail-item"> |
||||
|
<span class="label">当前频率:</span> |
||||
|
<span class="value">{{ getValueById(realtimeDeviceData, shorepowerSelectedItem.frequencyDeviceId, |
||||
|
'measureValue') }}</span> |
||||
|
</div> |
||||
|
<div class="detail-item"> |
||||
|
<span class="label">当前总用量:</span> |
||||
|
<span class="value">{{ getValueById(realtimeDeviceData, shorepowerSelectedItem.totalPowerDeviceId, |
||||
|
'measureValue') }}</span> |
||||
|
</div> |
||||
|
<div class="detail-item"> |
||||
|
<span class="label">当前状态:</span> |
||||
|
<span class="value">{{ shorepowerSelectedItem.storePowerStatus }}</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> |
||||
|
<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> |
||||
|
<ShipHistoryDialog v-model="shipHistoryVisible.visible" :ship-param="shipHistoryVisible.searchParams" |
||||
|
:realtime-device-data="realtimeDeviceData" /> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
import { ref, computed } from 'vue' |
||||
|
import ShipHistoryDialog from './ShipHistoryDialog.vue' |
||||
|
import { MapApi } from "@/api/shorepower/map"; |
||||
|
import { RealtimeDeviceData, ShipRespVo, ShorePowerBerth } from '@/types/shorepower'; |
||||
|
import { BERTH_TYPE, getOperationTypeLabel, SHORE_POWER_STATUS, UNUSED_SHORE_POWER_REASON } from './dictionaryTable'; |
||||
|
import { formatDuration, formatTimestamp, getValueById, showStatus } from './utils'; |
||||
|
// 定义组件属性 |
||||
|
type ShipItem = ShipRespVo & { id: string; modelInstance?: any; }; |
||||
|
|
||||
|
interface Props { |
||||
|
shipStatusData: ShipItem[]; |
||||
|
shorePowerStatusData: ShipItem[]; |
||||
|
realtimeDeviceData: RealtimeDeviceData[]; |
||||
|
shorePowerList: (ShorePowerBerth & { position: string; })[]; |
||||
|
shipDataList: ShipRespVo[]; |
||||
|
// selectedItem: ShipItem | null; |
||||
|
} |
||||
|
|
||||
|
const props = defineProps<Props>() |
||||
|
|
||||
|
// 搜索关键字 |
||||
|
const searchKeyword = ref('') |
||||
|
const storeSearchKeyword = ref('') |
||||
|
const show_type = ref('ship') |
||||
|
const shipSelectedItem = ref<ShipItem | null>(null) |
||||
|
const shorepowerSelectedItem = ref<ShorePowerBerth & { position: string; } | null>(null) |
||||
|
|
||||
|
|
||||
|
const shipHistoryVisible = ref({ |
||||
|
visible: false, |
||||
|
searchParams: { |
||||
|
shipId: 0 as number | null, |
||||
|
ids: [] as number[] | null, |
||||
|
type: 1 as number |
||||
|
} |
||||
|
}) |
||||
|
const currentShorePower = ref<ShorePowerBerth | null>(null) |
||||
|
const currentShip = ref<ShipItem | null>(null) |
||||
|
|
||||
|
// const ShorePowerList = ref<(ShorePowerBerth & { position: string; })[]>([]) |
||||
|
|
||||
|
// 过滤后的船舶数据 |
||||
|
const filteredShipStatusData = computed(() => { |
||||
|
if (!searchKeyword.value) { |
||||
|
return props.shipDataList |
||||
|
} |
||||
|
return props.shipDataList.filter(ship => |
||||
|
ship.shipBasicInfo.name.toLowerCase().includes(searchKeyword.value.toLowerCase()) |
||||
|
) |
||||
|
}) |
||||
|
|
||||
|
// 过滤后的岸电箱数据 |
||||
|
const filteredShorePowerList = computed(() => { |
||||
|
if (!storeSearchKeyword.value) { |
||||
|
return props.shorePowerList |
||||
|
} |
||||
|
return props.shorePowerList.filter(shorepower => |
||||
|
shorepower.name.toLowerCase().includes(storeSearchKeyword.value.toLowerCase()) |
||||
|
) |
||||
|
}) |
||||
|
|
||||
|
const handleSelectItem = async (item: ShipRespVo) => { |
||||
|
show_type.value = 'ship' |
||||
|
const deviceId = item.shorePower?.totalPowerDeviceId; |
||||
|
const res = await MapApi.getRealtimeDataByIdList({ ids: deviceId }) |
||||
|
console.log(res) |
||||
|
shipSelectedItem.value = { |
||||
|
...item, |
||||
|
shorePowerEquipment: { |
||||
|
...item.shorePowerEquipment, |
||||
|
'measureValue': res[0].measureValue || 'N/A', |
||||
|
} |
||||
|
} |
||||
|
emit('item-click', { |
||||
|
type: 'ship', |
||||
|
item: item |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// 处理搜索输入 |
||||
|
const handleSearch = () => { |
||||
|
// 输入处理逻辑(如果需要额外处理可以在这里添加) |
||||
|
} |
||||
|
|
||||
|
// 处理岸电箱搜索输入 |
||||
|
const handlStoreSearch = () => { |
||||
|
// 输入处理逻辑(如果需要额外处理可以在这里添加) |
||||
|
} |
||||
|
|
||||
|
// 显示岸电箱历史记录 |
||||
|
const showShorePowerHistory = (shorepower: ShorePowerBerth) => { |
||||
|
console.log('shorepower', shorepower) |
||||
|
currentShorePower.value = shorepower |
||||
|
/* shorePowerHistoryVisible.value = { |
||||
|
visible: true, |
||||
|
shorePowerId: shorepower.id || 0 |
||||
|
} */ |
||||
|
shipHistoryVisible.value = { |
||||
|
visible: true, |
||||
|
searchParams: { |
||||
|
shipId: null, |
||||
|
ids: [shorepower.id], |
||||
|
type: 5, |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 显示船舶历史记录 |
||||
|
const showShipHistory = (ship: ShipItem) => { |
||||
|
console.log('ship', ship) |
||||
|
currentShip.value = ship |
||||
|
shipHistoryVisible.value = { |
||||
|
visible: true, |
||||
|
searchParams: { |
||||
|
shipId: ship.shipBasicInfo.id, |
||||
|
ids: null, |
||||
|
type: 1, |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 定义事件 |
||||
|
const emit = defineEmits<{ |
||||
|
(e: 'switch-ship', ship: ShipItem): void; |
||||
|
(e: 'item-click', item: any): void; |
||||
|
// handleSelectItem(ship) |
||||
|
|
||||
|
}>() |
||||
|
|
||||
|
// 处理船舶选择 |
||||
|
const handleSwitch = (ship: ShipItem, type: string) => { |
||||
|
// console.log(ship) |
||||
|
show_type.value = type |
||||
|
emit('switch-ship', ship) |
||||
|
handleSelectItem(ship) |
||||
|
} |
||||
|
|
||||
|
const handleSelectShorePower = async (shorepower: ShorePowerBerth & { position: string }) => { |
||||
|
show_type.value = 'shorepower' |
||||
|
// selectedItem.value = shorepower |
||||
|
shorepowerSelectedItem.value = shorepower |
||||
|
emit('item-click', { |
||||
|
type: 'shorepower_box', |
||||
|
item: shorepower |
||||
|
}) |
||||
|
// const data = await MapApi.getRealtimeDataByIdList({ ids: shorepower.totalPowerDeviceId }) |
||||
|
// console.log('voltageDeviceId', data) |
||||
|
} |
||||
|
|
||||
|
const getStatusClass = (status: string | undefined) => { |
||||
|
switch (status) { |
||||
|
case '正常': |
||||
|
return 'status-normal' |
||||
|
case '在线': |
||||
|
return 'status-normal' |
||||
|
case '空闲': |
||||
|
return 'status-idle' |
||||
|
case '故障': |
||||
|
return 'status-fault' |
||||
|
case '超容': |
||||
|
return 'status-maintenance' |
||||
|
case '异常': |
||||
|
return 'status-abnormal' |
||||
|
case '维修中': |
||||
|
return 'status-maintenance' |
||||
|
case '岸电使用中': |
||||
|
return 'status-shorepower' |
||||
|
// case '岸电故障': |
||||
|
// return 'status-fault' |
||||
|
default: |
||||
|
return 'status-default' |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
watch(() => props.shipDataList, (newVal) => { |
||||
|
console.log('newVal', newVal) |
||||
|
}) |
||||
|
|
||||
|
onMounted(() => { |
||||
|
// console.log(props.shorePowerStatusData) |
||||
|
}) |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.port-overview { |
||||
|
display: flex; |
||||
|
gap: 10px; |
||||
|
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); |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,426 @@ |
|||||
|
<template> |
||||
|
<el-dialog v-model="visible" title="历史记录" width="1200px" :before-close="handleClose"> |
||||
|
<!-- <el-tabs v-model="activeTab"> --> |
||||
|
<!-- 船舶靠泊历史记录 tab --> |
||||
|
<!-- <el-tab-pane label="船舶靠泊历史记录" name="shipBerthing"> --> |
||||
|
<!-- 搜索和筛选区域 --> |
||||
|
<div class="filter-container"> |
||||
|
<el-form :model="berthingQueryParams" label-width="80px" inline> |
||||
|
<el-form-item label="类型"> |
||||
|
<el-select v-model="berthingQueryParams.type" style="width: 120px" placeholder="请选择类型"> |
||||
|
<el-option v-for="item in FACILITY_TYPE" :key="item.value" :label="item.label" :value="item.value" /> |
||||
|
</el-select> |
||||
|
</el-form-item> |
||||
|
<!-- <el-form-item label="关键字"> |
||||
|
<el-input v-model="berthingQueryParams.keyword" placeholder="请输入关键字搜索" clearable |
||||
|
@keyup.enter="handleBerthingSearch" /> |
||||
|
</el-form-item> --> |
||||
|
<el-form-item label="时间范围"> |
||||
|
<el-date-picker v-model="berthingDateRange" type="daterange" range-separator="至" start-placeholder="开始日期" |
||||
|
end-placeholder="结束日期" value-format="YYYY-MM-DD" @change="handleBerthingDateChange" /> |
||||
|
</el-form-item> |
||||
|
<el-form-item> |
||||
|
<el-button type="primary" @click="handleShorePowerConnectionSearch">搜索</el-button> |
||||
|
<el-button @click="resetBerthingQuery">重置</el-button> |
||||
|
</el-form-item> |
||||
|
</el-form> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 数据表格 --> |
||||
|
<el-table :data="berthingTableData" border stripe style="width: 100%" v-loading="berthingLoading"> |
||||
|
<el-table-column v-for="column in berthingColumns" :key="column.prop" :prop="column.prop" :label="column.label" |
||||
|
:width="column.width" :formatter="column.formatter" /> |
||||
|
</el-table> |
||||
|
|
||||
|
<!-- 分页 --> |
||||
|
<div class="pagination-container"> |
||||
|
<el-pagination v-model:current-page="berthingQueryParams.pageNo" v-model:page-size="berthingQueryParams.pageSize" |
||||
|
:page-sizes="[10, 20, 50, 100]" :total="berthingTotal" layout="total, sizes, prev, pager, next, jumper" |
||||
|
@size-change="handleBerthingSizeChange" @current-change="handleBerthingCurrentChange" /> |
||||
|
</div> |
||||
|
<!-- </el-tab-pane> --> |
||||
|
|
||||
|
|
||||
|
<!-- <el-tab-pane label="船舶连接岸电箱历史记录" name="shorePowerConnection"> |
||||
|
|
||||
|
<div class="filter-container"> |
||||
|
<el-form :model="shorePowerConnectionQueryParams" label-width="80px" inline> |
||||
|
<el-form-item label="关键字"> |
||||
|
<el-input v-model="shorePowerConnectionQueryParams.keyword" placeholder="请输入关键字搜索" clearable |
||||
|
@keyup.enter="handleShorePowerConnectionSearch" /> |
||||
|
</el-form-item> |
||||
|
<el-form-item label="时间范围"> |
||||
|
<el-date-picker v-model="shorePowerConnectionDateRange" type="daterange" range-separator="至" |
||||
|
start-placeholder="开始日期" end-placeholder="结束日期" value-format="YYYY-MM-DD" |
||||
|
@change="handleShorePowerConnectionDateChange" /> |
||||
|
</el-form-item> |
||||
|
<el-form-item> |
||||
|
<el-button type="primary" @click="handleShorePowerConnectionSearch">搜索</el-button> |
||||
|
<el-button @click="resetShorePowerConnectionQuery">重置</el-button> |
||||
|
</el-form-item> |
||||
|
</el-form> |
||||
|
</div> |
||||
|
|
||||
|
|
||||
|
<el-table :data="shorePowerConnectionTableData" border stripe style="width: 100%" |
||||
|
v-loading="shorePowerConnectionLoading"> |
||||
|
<el-table-column v-for="column in shorePowerConnectionColumns" :key="column.prop" :prop="column.prop" |
||||
|
:label="column.label" :width="column.width" :formatter="column.formatter" /> |
||||
|
</el-table> |
||||
|
|
||||
|
|
||||
|
<div class="pagination-container"> |
||||
|
<el-pagination v-model:current-page="shorePowerConnectionQueryParams.pageNo" |
||||
|
v-model:page-size="shorePowerConnectionQueryParams.pageSize" :page-sizes="[10, 20, 50, 100]" |
||||
|
:total="shorePowerConnectionTotal" layout="total, sizes, prev, pager, next, jumper" |
||||
|
@size-change="handleShorePowerConnectionSizeChange" |
||||
|
@current-change="handleShorePowerConnectionCurrentChange" /> |
||||
|
</div> --> |
||||
|
<!-- </el-tab-pane> --> |
||||
|
<!-- </el-tabs> --> |
||||
|
|
||||
|
<template #footer> |
||||
|
<span class="dialog-footer"> |
||||
|
<el-button @click="handleClose">关闭</el-button> |
||||
|
</span> |
||||
|
</template> |
||||
|
</el-dialog> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
import { ref, computed, watch } from 'vue' |
||||
|
import { MapApi } from "@/api/shorepower/map"; |
||||
|
import { |
||||
|
ElDialog, |
||||
|
ElTabs, |
||||
|
ElTabPane, |
||||
|
ElForm, |
||||
|
ElFormItem, |
||||
|
ElInput, |
||||
|
ElDatePicker, |
||||
|
ElButton, |
||||
|
ElTable, |
||||
|
ElTableColumn, |
||||
|
ElPagination |
||||
|
} from 'element-plus' |
||||
|
import { CARGO_CATEGORY, FACILITY_TYPE, HARBOR_DISTRICT, getOperationTypeLabel, OPERATION_TYPE } from './dictionaryTable'; |
||||
|
import { formatDuration, formatTimestamp, showStatus } from './utils'; |
||||
|
import { RealtimeDeviceData } from '@/types/shorepower'; |
||||
|
|
||||
|
// 定义组件属性 |
||||
|
interface Props { |
||||
|
modelValue: boolean |
||||
|
berthingData?: any[] |
||||
|
shipParam?: { type: number, ids: number[] | null, shipId: number | null } |
||||
|
shorePowerConnectionData?: any[] |
||||
|
realtimeDeviceData: RealtimeDeviceData[]; |
||||
|
} |
||||
|
|
||||
|
// 定义事件 |
||||
|
const emit = defineEmits<{ |
||||
|
(e: 'update:modelValue', value: boolean): void |
||||
|
(e: 'berthing-search', params: any): void |
||||
|
(e: 'shore-power-connection-search', params: any): void |
||||
|
}>() |
||||
|
|
||||
|
// 属性定义 |
||||
|
const props = withDefaults(defineProps<Props>(), { |
||||
|
modelValue: false, |
||||
|
shipId: 0, |
||||
|
berthingData: () => [], |
||||
|
shorePowerConnectionData: () => [] |
||||
|
}) |
||||
|
|
||||
|
// 响应式数据 |
||||
|
const visible = computed({ |
||||
|
get: () => props.modelValue, |
||||
|
set: (val) => emit('update:modelValue', val) |
||||
|
}) |
||||
|
|
||||
|
// 船舶靠泊历史记录相关数据 |
||||
|
const berthingQueryParams = ref({ |
||||
|
// keyword: '', |
||||
|
startTime: '', |
||||
|
endTime: '', |
||||
|
ids: null as number[] | null, |
||||
|
shipId: null as number | null, |
||||
|
type: 1 as number | null, |
||||
|
pageNo: 1, |
||||
|
pageSize: 10 |
||||
|
}) |
||||
|
|
||||
|
const berthingDateRange = ref<[string, string]>(['', '']) |
||||
|
const berthingLoading = ref(false) |
||||
|
const berthingTotal = ref(0) |
||||
|
|
||||
|
// 船舶靠泊历史记录表格数据 |
||||
|
const berthingTableData = ref<any[]>([]) |
||||
|
|
||||
|
// 船舶靠泊历史记录表格列 |
||||
|
const berthingColumns = ref([ |
||||
|
{ |
||||
|
prop: 'shipBasicInfo', |
||||
|
label: '船舶名称-英文名称', |
||||
|
width: 200, |
||||
|
formatter: (row) => { |
||||
|
const name = row.shipBasicInfo?.name || ''; |
||||
|
const nameEn = row.shipBasicInfo?.nameEn || ''; |
||||
|
return `${name}${name && nameEn ? '-' : ''}${nameEn}`; |
||||
|
} |
||||
|
}, |
||||
|
{ prop: 'shipBasicInfo.length', label: '船长', width: 200 }, |
||||
|
{ prop: 'shipBasicInfo.width', label: '船宽', width: 150 }, |
||||
|
{ prop: 'shipBasicInfo.tonnage', label: '最大载重', width: 150 }, |
||||
|
{ prop: 'shipBasicInfo.voyage', label: '航线', width: 150 }, |
||||
|
{ prop: 'applyInfo.departureHarborDistrict', label: '起运港', width: 150 }, |
||||
|
{ prop: 'arrivalHarborDistrict', label: '到达港', width: 150 }, |
||||
|
{ prop: 'applyInfo.operationType', label: '靠泊类型', width: 150, formatter: (_row, _column, cellValue) => getOperationTypeLabel(cellValue, OPERATION_TYPE) }, |
||||
|
{ prop: '', label: '作业内容', width: 180 }, |
||||
|
/* { |
||||
|
prop: 'applyInfo.loadingCargoCategory', label: '货物类型', width: 180, |
||||
|
formatter: (row) => { |
||||
|
if (row.applyInfo.operationType === 1) { |
||||
|
return getOperationTypeLabel(row.applyInfo.loadingCargoCategory, CARGO_CATEGORY); |
||||
|
} else if (row.applyInfo.operationType === 2) { |
||||
|
return row.applyInfo.unloadingCargoCategory; |
||||
|
} else if (row.applyInfo.operationType === 3) { |
||||
|
return '装' + row.applyInfo.loadingCargoCategory + '-卸' + row.applyInfo.unloadingCargoCategory |
||||
|
} |
||||
|
return ''; |
||||
|
} |
||||
|
}, */ |
||||
|
|
||||
|
{ |
||||
|
prop: 'applyInfo.loadingCargoCategory', label: '货物名称', width: 180, |
||||
|
formatter: (row) => { |
||||
|
if (row.applyInfo.operationType === 1) { |
||||
|
const label = getOperationTypeLabel(row.applyInfo.loadingCargoCategory, CARGO_CATEGORY); |
||||
|
return label; |
||||
|
} else if (row.applyInfo.operationType === 2) { |
||||
|
const label = getOperationTypeLabel(row.applyInfo.unloadingCargoCategory, CARGO_CATEGORY); |
||||
|
return label; |
||||
|
} else if (row.applyInfo.operationType === 3) { |
||||
|
return '装' + getOperationTypeLabel(row.applyInfo.loadingCargoCategory, CARGO_CATEGORY) + '-卸' + getOperationTypeLabel(row.applyInfo.unloadingCargoCategory, CARGO_CATEGORY) |
||||
|
} |
||||
|
return ''; |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
prop: 'applyInfo.loadingCargoCategory', label: '货物吨数', width: 180, |
||||
|
formatter: (row) => { |
||||
|
if (row.applyInfo.operationType === 1) { |
||||
|
return row.applyInfo.loadingCargoTonnage; |
||||
|
} else if (row.applyInfo.operationType === 2) { |
||||
|
return row.applyInfo.unloadingCargoTonnage; |
||||
|
} else if (row.applyInfo.operationType === 3) { |
||||
|
return '装' + row.applyInfo.loadingCargoTonnage + '-卸' + row.applyInfo.unloadingCargoTonnage |
||||
|
} |
||||
|
return ''; |
||||
|
} |
||||
|
}, |
||||
|
{ prop: 'usageRecordInfo.actualBerthTime', label: '靠泊时间', width: 180, formatter: (row) => formatTimestamp(row.usageRecordInfo.actualBerthTime) }, |
||||
|
{ prop: 'usageRecordInfo.actualDepartureTime', label: '离泊时间', width: 180, formatter: (row) => formatTimestamp(row.usageRecordInfo.actualDepartureTime) }, |
||||
|
{ |
||||
|
prop: 'useStatus.status', label: '岸电使用情况', width: 180, |
||||
|
formatter: (row) => { |
||||
|
const status = showStatus(row, props.realtimeDeviceData); |
||||
|
return status?.status || ''; |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
]) |
||||
|
|
||||
|
const shorePowerConnectionTotal = ref(0) |
||||
|
|
||||
|
// 处理日期变化 |
||||
|
const handleBerthingDateChange = (val: [string, string]) => { |
||||
|
if (val && val.length === 2) { |
||||
|
berthingQueryParams.value.startTime = val[0] + ' 00:00:00' |
||||
|
berthingQueryParams.value.endTime = val[1] + ' 23:59:59' |
||||
|
} else { |
||||
|
berthingQueryParams.value.startTime = '' |
||||
|
berthingQueryParams.value.endTime = '' |
||||
|
} |
||||
|
handleShorePowerConnectionSearch() |
||||
|
} |
||||
|
|
||||
|
// 处理关闭 |
||||
|
const handleClose = () => { |
||||
|
berthingQueryParams.value.ids = null |
||||
|
berthingQueryParams.value.shipId = null |
||||
|
berthingQueryParams.value.type = 1 |
||||
|
visible.value = false |
||||
|
} |
||||
|
|
||||
|
// 船舶靠泊历史记录相关方法 |
||||
|
const handleBerthingSearch = () => { |
||||
|
// 发送搜索事件给父组件 |
||||
|
emit('berthing-search', { ...berthingQueryParams.value }) |
||||
|
} |
||||
|
|
||||
|
const resetBerthingQuery = () => { |
||||
|
// berthingQueryParams.value.keyword = '' |
||||
|
berthingQueryParams.value.startTime = '' |
||||
|
berthingQueryParams.value.endTime = '' |
||||
|
berthingDateRange.value = ['', ''] |
||||
|
berthingQueryParams.value.pageNo = 1 |
||||
|
handleShorePowerConnectionSearch() |
||||
|
} |
||||
|
|
||||
|
|
||||
|
const handleBerthingSizeChange = (val: number) => { |
||||
|
berthingQueryParams.value.pageSize = val |
||||
|
berthingQueryParams.value.pageNo = 1 |
||||
|
handleShorePowerConnectionSearch() |
||||
|
} |
||||
|
|
||||
|
const handleBerthingCurrentChange = (val: number) => { |
||||
|
berthingQueryParams.value.pageNo = val |
||||
|
handleShorePowerConnectionSearch() |
||||
|
} |
||||
|
|
||||
|
// 船舶连接岸电箱历史记录相关方法 |
||||
|
const handleShorePowerConnectionSearch = () => { |
||||
|
// const |
||||
|
const params = { |
||||
|
// shipId: berthingQueryParams.value.shipId, |
||||
|
// ids: berthingQueryParams.value.ids, |
||||
|
...(berthingQueryParams.value.ids ? { ids: berthingQueryParams.value.ids } : {}), |
||||
|
...(berthingQueryParams.value.shipId ? { shipId: berthingQueryParams.value.shipId } : {}), |
||||
|
type: berthingQueryParams.value.type, |
||||
|
pageNo: 1, |
||||
|
pageSize: 9999 // 请求足够多的数据,以便在前端进行筛选和分页 |
||||
|
} |
||||
|
handleGetShipHistortyList(params) |
||||
|
// 发送搜索事件给父组件 |
||||
|
// emit('shore-power-connection-search', { ...shorePowerConnectionQueryParams.value }) |
||||
|
} |
||||
|
|
||||
|
// 监听数据变化并更新总数 |
||||
|
watch( |
||||
|
() => props.berthingData, |
||||
|
(newData) => { |
||||
|
berthingTotal.value = newData?.length || 0 |
||||
|
}, |
||||
|
{ immediate: true } |
||||
|
) |
||||
|
|
||||
|
watch( |
||||
|
() => props.shorePowerConnectionData, |
||||
|
(newData) => { |
||||
|
shorePowerConnectionTotal.value = newData?.length || 0 |
||||
|
}, |
||||
|
{ immediate: true } |
||||
|
) |
||||
|
|
||||
|
watch( |
||||
|
() => props.shipParam, |
||||
|
(newShipParam) => { |
||||
|
if (newShipParam) { |
||||
|
// 当船舶ID变化时,触发搜索 |
||||
|
berthingTableData.value = [] |
||||
|
if (newShipParam.shipId) { |
||||
|
berthingQueryParams.value.shipId = newShipParam.shipId |
||||
|
} else { |
||||
|
berthingQueryParams.value.shipId = null |
||||
|
} |
||||
|
if (newShipParam.ids && newShipParam.ids.length > 0) { |
||||
|
berthingQueryParams.value.ids = newShipParam.ids |
||||
|
} else { |
||||
|
berthingQueryParams.value.ids = null |
||||
|
} |
||||
|
berthingQueryParams.value.type = newShipParam.type |
||||
|
// handleGetShipHistortyList() |
||||
|
handleShorePowerConnectionSearch() |
||||
|
} |
||||
|
} |
||||
|
) |
||||
|
const handleGetShipHistortyList = async (param) => { |
||||
|
console.log('handleGetShipHistortyList', param) |
||||
|
if (!param) return; |
||||
|
// try { |
||||
|
const res = await MapApi.getShipHistoryPage({ |
||||
|
/* shipId: param.id, // 将字符串转换为数字 |
||||
|
pageNo: 1, |
||||
|
pageSize: 10, |
||||
|
type: param.type, */ |
||||
|
// ids: [parseInt(id, 10)] |
||||
|
...param |
||||
|
}) |
||||
|
console.log(res); |
||||
|
|
||||
|
// 本地日期筛选逻辑 |
||||
|
let filteredList = [...res.list]; |
||||
|
if (berthingQueryParams.value.startTime || berthingQueryParams.value.endTime) { |
||||
|
filteredList = filteredList.filter(item => { |
||||
|
const berthTime = item.usageRecordInfo?.actualBerthTime; |
||||
|
if (!berthTime) return false; |
||||
|
|
||||
|
const berthTimestamp = new Date(berthTime).getTime(); |
||||
|
const startTime = berthingQueryParams.value.startTime ? new Date(berthingQueryParams.value.startTime).getTime() : 0; |
||||
|
const endTime = berthingQueryParams.value.endTime ? new Date(berthingQueryParams.value.endTime).getTime() : Infinity; |
||||
|
|
||||
|
// 检查靠泊时间是否在筛选范围内 |
||||
|
if (berthTimestamp >= startTime && berthTimestamp <= endTime) { |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
// 如果有离泊时间,也检查离泊时间是否在筛选范围内 |
||||
|
if (item.usageRecordInfo?.actualDepartureTime) { |
||||
|
const departureTimestamp = new Date(item.usageRecordInfo.actualDepartureTime).getTime(); |
||||
|
if (departureTimestamp >= startTime && departureTimestamp <= endTime) { |
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 检查筛选范围是否与靠泊-离泊时间段有重叠 |
||||
|
if (item.usageRecordInfo?.actualDepartureTime) { |
||||
|
const departureTimestamp = new Date(item.usageRecordInfo.actualDepartureTime).getTime(); |
||||
|
if (berthTimestamp <= endTime && departureTimestamp >= startTime) { |
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return false; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// 前端分页逻辑 |
||||
|
const totalFiltered = filteredList.length; |
||||
|
const pageNo = berthingQueryParams.value.pageNo; |
||||
|
const pageSize = berthingQueryParams.value.pageSize; |
||||
|
const startIndex = (pageNo - 1) * pageSize; |
||||
|
const endIndex = startIndex + pageSize; |
||||
|
const paginatedList = filteredList.slice(startIndex, endIndex); |
||||
|
const harborDistructList = await HARBOR_DISTRICT() |
||||
|
const buildData = await Promise.all(paginatedList.map(async item => ({ |
||||
|
...item, |
||||
|
arrivalHarborDistrict: getOperationTypeLabel(item.applyInfo.arrivalHarborDistrict, harborDistructList), |
||||
|
}))) |
||||
|
berthingTableData.value = buildData |
||||
|
berthingTotal.value = totalFiltered |
||||
|
} |
||||
|
|
||||
|
onMounted(() => { |
||||
|
console.log('页面加载了!') |
||||
|
// console.log('props.shipId', props.shipId) |
||||
|
}) |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.filter-container { |
||||
|
margin-bottom: 20px; |
||||
|
} |
||||
|
|
||||
|
.pagination-container { |
||||
|
margin-top: 20px; |
||||
|
display: flex; |
||||
|
justify-content: flex-end; |
||||
|
} |
||||
|
|
||||
|
.dialog-footer { |
||||
|
display: flex; |
||||
|
justify-content: flex-end; |
||||
|
gap: 10px; |
||||
|
} |
||||
|
</style> |
||||
File diff suppressed because it is too large
@ -0,0 +1,343 @@ |
|||||
|
<template> |
||||
|
<el-dialog v-model="visible" title="岸电箱历史记录" width="1200px" :before-close="handleClose"> |
||||
|
<el-tabs v-model="activeTab"> |
||||
|
<!-- 岸电箱历史记录 tab --> |
||||
|
<el-tab-pane label="岸电箱历史记录" name="shorePower"> |
||||
|
<!-- 搜索和筛选区域 --> |
||||
|
<div class="filter-container"> |
||||
|
<el-form :model="queryParams" label-width="80px" inline> |
||||
|
<el-form-item label="关键字"> |
||||
|
<el-input v-model="queryParams.keyword" placeholder="请输入关键字搜索" clearable @keyup.enter="handleSearch" /> |
||||
|
</el-form-item> |
||||
|
<el-form-item label="时间范围"> |
||||
|
<el-date-picker v-model="dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" |
||||
|
end-placeholder="结束日期" value-format="YYYY-MM-DD" @change="handleDateChange" /> |
||||
|
</el-form-item> |
||||
|
<el-form-item> |
||||
|
<el-button type="primary" @click="handleSearch">搜索</el-button> |
||||
|
<el-button @click="resetQuery">重置</el-button> |
||||
|
</el-form-item> |
||||
|
</el-form> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 数据表格 --> |
||||
|
<el-table :data="shorePowerTableData" border stripe style="width: 100%" v-loading="loading"> |
||||
|
<el-table-column v-for="column in tableColumns" :key="column.prop" :prop="column.prop" :label="column.label" |
||||
|
:width="column.width" :formatter="column.formatter" /> |
||||
|
</el-table> |
||||
|
|
||||
|
<!-- 分页 --> |
||||
|
<div class="pagination-container"> |
||||
|
<el-pagination v-model:current-page="queryParams.pageNo" v-model:page-size="queryParams.pageSize" |
||||
|
:page-sizes="[10, 20, 50, 100]" :total="total" layout="total, sizes, prev, pager, next, jumper" |
||||
|
@size-change="handleSizeChange" @current-change="handleCurrentChange" /> |
||||
|
</div> |
||||
|
</el-tab-pane> |
||||
|
|
||||
|
<!-- 岸电箱连接船舶记录 tab --> |
||||
|
<el-tab-pane label="岸电箱连接船舶记录" name="shipConnection"> |
||||
|
<!-- 搜索和筛选区域 --> |
||||
|
<div class="filter-container"> |
||||
|
<el-form :model="shipConnectionQueryParams" label-width="80px" inline> |
||||
|
<el-form-item label="关键字"> |
||||
|
<el-input v-model="shipConnectionQueryParams.keyword" placeholder="请输入关键字搜索" clearable |
||||
|
@keyup.enter="handleShipConnectionSearch" /> |
||||
|
</el-form-item> |
||||
|
<el-form-item label="时间范围"> |
||||
|
<el-date-picker v-model="shipConnectionDateRange" type="daterange" range-separator="至" |
||||
|
start-placeholder="开始日期" end-placeholder="结束日期" value-format="YYYY-MM-DD" |
||||
|
@change="handleShipConnectionDateChange" /> |
||||
|
</el-form-item> |
||||
|
<el-form-item> |
||||
|
<el-button type="primary" @click="handleShipConnectionSearch">搜索</el-button> |
||||
|
<el-button @click="resetShipConnectionQuery">重置</el-button> |
||||
|
</el-form-item> |
||||
|
</el-form> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 数据表格 --> |
||||
|
<el-table :data="shipConnectionTableData" border stripe style="width: 100%" v-loading="shipConnectionLoading"> |
||||
|
<el-table-column v-for="column in shipConnectionColumns" :key="column.prop" :prop="column.prop" |
||||
|
:label="column.label" :width="column.width" :formatter="column.formatter" /> |
||||
|
</el-table> |
||||
|
|
||||
|
<!-- 分页 --> |
||||
|
<div class="pagination-container"> |
||||
|
<el-pagination v-model:current-page="shipConnectionQueryParams.pageNo" |
||||
|
v-model:page-size="shipConnectionQueryParams.pageSize" :page-sizes="[10, 20, 50, 100]" |
||||
|
:total="shipConnectionTotal" layout="total, sizes, prev, pager, next, jumper" |
||||
|
@size-change="handleShipConnectionSizeChange" @current-change="handleShipConnectionCurrentChange" /> |
||||
|
</div> |
||||
|
</el-tab-pane> |
||||
|
</el-tabs> |
||||
|
|
||||
|
<template #footer> |
||||
|
<span class="dialog-footer"> |
||||
|
<el-button @click="handleClose">关闭</el-button> |
||||
|
</span> |
||||
|
</template> |
||||
|
</el-dialog> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
import { ref, computed, watch } from 'vue' |
||||
|
import { |
||||
|
ElDialog, |
||||
|
ElTabs, |
||||
|
ElTabPane, |
||||
|
ElForm, |
||||
|
ElFormItem, |
||||
|
ElInput, |
||||
|
ElDatePicker, |
||||
|
ElButton, |
||||
|
ElTable, |
||||
|
ElTableColumn, |
||||
|
ElPagination |
||||
|
} from 'element-plus' |
||||
|
import { MapApi } from "@/api/shorepower/map"; |
||||
|
|
||||
|
// 定义组件属性 |
||||
|
interface Props { |
||||
|
modelValue: boolean |
||||
|
data?: any[] |
||||
|
shipConnectionData?: any[] |
||||
|
shorePowerId: number |
||||
|
} |
||||
|
|
||||
|
// 定义事件 |
||||
|
const emit = defineEmits<{ |
||||
|
(e: 'update:modelValue', value: boolean): void |
||||
|
(e: 'search', params: any): void |
||||
|
(e: 'ship-connection-search', params: any): void |
||||
|
}>() |
||||
|
|
||||
|
// 属性定义 |
||||
|
const props = withDefaults(defineProps<Props>(), { |
||||
|
modelValue: false, |
||||
|
shorePowerId: 0, |
||||
|
data: () => [], |
||||
|
shipConnectionData: () => [] |
||||
|
}) |
||||
|
|
||||
|
// 响应式数据 |
||||
|
const visible = computed({ |
||||
|
get: () => props.modelValue, |
||||
|
set: (val) => emit('update:modelValue', val) |
||||
|
}) |
||||
|
|
||||
|
const shorePowerTableData = ref<any[]>([]) |
||||
|
const activeTab = ref('shorePower') |
||||
|
|
||||
|
// 岸电箱历史记录相关数据 |
||||
|
const queryParams = ref({ |
||||
|
keyword: '', |
||||
|
startTime: '', |
||||
|
endTime: '', |
||||
|
pageNo: 1, |
||||
|
pageSize: 10 |
||||
|
}) |
||||
|
|
||||
|
const dateRange = ref<[string, string]>(['', '']) |
||||
|
const loading = ref(false) |
||||
|
const total = ref(0) |
||||
|
|
||||
|
// 表格数据计算属性 |
||||
|
const tableData = computed(() => { |
||||
|
// 在实际应用中,这里应该是从服务器获取的数据 |
||||
|
// 当前为了演示,我们使用传入的模拟数据 |
||||
|
return props.data?.slice( |
||||
|
(queryParams.value.pageNo - 1) * queryParams.value.pageSize, |
||||
|
queryParams.value.pageNo * queryParams.value.pageSize |
||||
|
) || [] |
||||
|
}) |
||||
|
|
||||
|
// 表格列 |
||||
|
const tableColumns = ref([ |
||||
|
{ prop: 'id', label: '记录ID', width: 80 }, |
||||
|
{ prop: 'time', label: '时间', width: 180 }, |
||||
|
{ prop: 'operation', label: '操作类型' }, |
||||
|
{ prop: 'voltage', label: '电压(V)' }, |
||||
|
{ prop: 'current', label: '电流(A)' }, |
||||
|
{ prop: 'power', label: '功率(kW)' }, |
||||
|
{ prop: 'energy', label: '用电量(kWh)' } |
||||
|
]) |
||||
|
|
||||
|
// 岸电箱连接船舶记录相关数据 |
||||
|
const shipConnectionQueryParams = ref({ |
||||
|
keyword: '', |
||||
|
startTime: '', |
||||
|
endTime: '', |
||||
|
pageNo: 1, |
||||
|
pageSize: 10 |
||||
|
}) |
||||
|
|
||||
|
const shipConnectionDateRange = ref<[string, string]>(['', '']) |
||||
|
const shipConnectionLoading = ref(false) |
||||
|
const shipConnectionTotal = ref(0) |
||||
|
|
||||
|
// 岸电箱连接船舶记录表格数据 |
||||
|
const shipConnectionTableData = computed(() => { |
||||
|
// 在实际应用中,这里应该是从服务器获取的数据 |
||||
|
// 当前为了演示,我们使用传入的模拟数据 |
||||
|
return props.shipConnectionData?.slice( |
||||
|
(shipConnectionQueryParams.value.pageNo - 1) * shipConnectionQueryParams.value.pageSize, |
||||
|
shipConnectionQueryParams.value.pageNo * shipConnectionQueryParams.value.pageSize |
||||
|
) || [] |
||||
|
}) |
||||
|
|
||||
|
// 岸电箱连接船舶记录表格列 |
||||
|
const shipConnectionColumns = ref([ |
||||
|
{ prop: 'id', label: '记录ID', width: 80 }, |
||||
|
{ prop: 'shipName', label: '船舶名称', width: 150 }, |
||||
|
{ prop: 'time', label: '连接时间', width: 180 }, |
||||
|
{ prop: 'duration', label: '连接时长(小时)', width: 120 }, |
||||
|
{ prop: 'powerConsumption', label: '用电量(kWh)', width: 120 }, |
||||
|
{ prop: 'status', label: '状态', width: 100 } |
||||
|
]) |
||||
|
|
||||
|
// 处理关闭 |
||||
|
const handleClose = () => { |
||||
|
visible.value = false |
||||
|
} |
||||
|
|
||||
|
// 岸电箱历史记录相关方法 |
||||
|
const handleSearch = () => { |
||||
|
// 发送搜索事件给父组件 |
||||
|
emit('search', { ...queryParams.value }) |
||||
|
} |
||||
|
|
||||
|
const resetQuery = () => { |
||||
|
queryParams.value.keyword = '' |
||||
|
queryParams.value.startTime = '' |
||||
|
queryParams.value.endTime = '' |
||||
|
dateRange.value = ['', ''] |
||||
|
queryParams.value.pageNo = 1 |
||||
|
handleSearch() |
||||
|
} |
||||
|
|
||||
|
const handleDateChange = (val: [string, string] | null) => { |
||||
|
if (val && val[0] && val[1]) { |
||||
|
queryParams.value.startTime = val[0] |
||||
|
queryParams.value.endTime = val[1] |
||||
|
} else { |
||||
|
queryParams.value.startTime = '' |
||||
|
queryParams.value.endTime = '' |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const handleSizeChange = (val: number) => { |
||||
|
queryParams.value.pageSize = val |
||||
|
queryParams.value.pageNo = 1 |
||||
|
handleSearch() |
||||
|
} |
||||
|
|
||||
|
const handleCurrentChange = (val: number) => { |
||||
|
queryParams.value.pageNo = val |
||||
|
handleSearch() |
||||
|
} |
||||
|
|
||||
|
// 岸电箱连接船舶记录相关方法 |
||||
|
const handleShipConnectionSearch = () => { |
||||
|
// 发送搜索事件给父组件 |
||||
|
emit('ship-connection-search', { ...shipConnectionQueryParams.value }) |
||||
|
} |
||||
|
|
||||
|
const resetShipConnectionQuery = () => { |
||||
|
shipConnectionQueryParams.value.keyword = '' |
||||
|
shipConnectionQueryParams.value.startTime = '' |
||||
|
shipConnectionQueryParams.value.endTime = '' |
||||
|
shipConnectionDateRange.value = ['', ''] |
||||
|
shipConnectionQueryParams.value.pageNo = 1 |
||||
|
handleShipConnectionSearch() |
||||
|
} |
||||
|
|
||||
|
const handleShipConnectionDateChange = (val: [string, string] | null) => { |
||||
|
if (val && val[0] && val[1]) { |
||||
|
shipConnectionQueryParams.value.startTime = val[0] |
||||
|
shipConnectionQueryParams.value.endTime = val[1] |
||||
|
} else { |
||||
|
shipConnectionQueryParams.value.startTime = '' |
||||
|
shipConnectionQueryParams.value.endTime = '' |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const handleShipConnectionSizeChange = (val: number) => { |
||||
|
shipConnectionQueryParams.value.pageSize = val |
||||
|
shipConnectionQueryParams.value.pageNo = 1 |
||||
|
handleShipConnectionSearch() |
||||
|
} |
||||
|
|
||||
|
const handleShipConnectionCurrentChange = (val: number) => { |
||||
|
shipConnectionQueryParams.value.pageNo = val |
||||
|
handleShipConnectionSearch() |
||||
|
} |
||||
|
|
||||
|
// 监听数据变化并更新总数 |
||||
|
watch( |
||||
|
() => props.data, |
||||
|
(newData) => { |
||||
|
total.value = newData?.length || 0 |
||||
|
}, |
||||
|
{ immediate: true } |
||||
|
) |
||||
|
|
||||
|
watch( |
||||
|
() => props.shipConnectionData, |
||||
|
(newData) => { |
||||
|
shipConnectionTotal.value = newData?.length || 0 |
||||
|
}, |
||||
|
{ immediate: true } |
||||
|
) |
||||
|
|
||||
|
watch( |
||||
|
() => props.shorePowerId, |
||||
|
(newShorePowerId) => { |
||||
|
if (newShorePowerId) { |
||||
|
// 当岸电箱ID变化时,触发搜索 |
||||
|
shorePowerTableData.value = [] |
||||
|
handleGetShorePowerHistoryList(newShorePowerId) |
||||
|
} |
||||
|
} |
||||
|
) |
||||
|
|
||||
|
const handleGetShorePowerHistoryList = async (id) => { |
||||
|
console.log('handleGetShorePowerHistoryList', id) |
||||
|
if (!id) return; |
||||
|
// try { |
||||
|
const res = await MapApi.getShipHistoryPage({ |
||||
|
// shipId: parseInt(id, 10), // 将字符串转换为数字 |
||||
|
pageNo: 1, |
||||
|
pageSize: 10, |
||||
|
type: 5, |
||||
|
ids: [parseInt(id, 10)] |
||||
|
}) |
||||
|
console.log(res); |
||||
|
const buildData = await Promise.all(res.list.map(async item => ({ |
||||
|
...item, |
||||
|
}))) |
||||
|
shorePowerTableData.value = buildData |
||||
|
} |
||||
|
|
||||
|
onMounted(() => { |
||||
|
console.log('页面加载了!') |
||||
|
console.log('props.shipId', props.shorePowerId) |
||||
|
}) |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.filter-container { |
||||
|
margin-bottom: 20px; |
||||
|
} |
||||
|
|
||||
|
.pagination-container { |
||||
|
margin-top: 20px; |
||||
|
display: flex; |
||||
|
justify-content: flex-end; |
||||
|
} |
||||
|
|
||||
|
.dialog-footer { |
||||
|
display: flex; |
||||
|
justify-content: flex-end; |
||||
|
gap: 10px; |
||||
|
} |
||||
|
</style> |
||||
File diff suppressed because it is too large
File diff suppressed because it is too large
@ -0,0 +1,184 @@ |
|||||
|
<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: '#4CAF50' // 固定使用绿色 |
||||
|
}) |
||||
|
|
||||
|
// 图表容器引用和实例 |
||||
|
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: '5%', |
||||
|
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: '#4CAF50' // 固定使用绿色 |
||||
|
}, |
||||
|
{ |
||||
|
offset: 1, |
||||
|
color: '#4CAF5080' // 绿色带透明度 |
||||
|
} |
||||
|
] |
||||
|
}, |
||||
|
borderRadius: [1, 1, 0, 0] |
||||
|
}, |
||||
|
emphasis: { |
||||
|
itemStyle: { |
||||
|
color: '#4CAF50' // 固定使用绿色高亮 |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
|
||||
|
chartInstance.setOption(option) |
||||
|
} |
||||
|
|
||||
|
// 监听数据变化 |
||||
|
watch( |
||||
|
() => props.chartData, |
||||
|
() => { |
||||
|
updateChart() |
||||
|
}, |
||||
|
{ deep: true } |
||||
|
) |
||||
|
|
||||
|
// 窗口大小改变时重绘图表 |
||||
|
const handleResize = () => { |
||||
|
if (chartInstance) { |
||||
|
chartInstance.resize() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 组件挂载时初始化图表 |
||||
|
onMounted(() => { |
||||
|
setTimeout(() => { |
||||
|
initChart() |
||||
|
}, 200) |
||||
|
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> |
||||
@ -0,0 +1,183 @@ |
|||||
|
<template> |
||||
|
<div ref="chartContainer" class="pie-chart-container"></div> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
import { ref, onMounted, onBeforeUnmount, watch } from 'vue' |
||||
|
import * as echarts from 'echarts' |
||||
|
|
||||
|
// 定义组件props |
||||
|
interface Props { |
||||
|
chartData?: Array<{ name: string; value: number }> |
||||
|
title?: string |
||||
|
} |
||||
|
|
||||
|
const props = withDefaults(defineProps<Props>(), { |
||||
|
chartData: () => [], |
||||
|
title: '' |
||||
|
}) |
||||
|
|
||||
|
const chartContainer = ref<HTMLDivElement | null>(null) |
||||
|
let chartInstance: echarts.ECharts | null = null |
||||
|
|
||||
|
// 初始化图表 |
||||
|
const initChart = () => { |
||||
|
if (chartContainer.value) { |
||||
|
chartInstance = echarts.init(chartContainer.value) |
||||
|
updateChart() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 计算数据总和 |
||||
|
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, |
||||
|
left: 'center', |
||||
|
textStyle: { |
||||
|
color: '#FFF' |
||||
|
} |
||||
|
}, |
||||
|
tooltip: { |
||||
|
trigger: 'item' |
||||
|
}, |
||||
|
legend: { |
||||
|
show: true, |
||||
|
// type: 'scroll', // 👈 开启可滚动图例 |
||||
|
orient: 'horizontal', // 👈 水平排列 |
||||
|
top: 10, // 👈 放在顶部 |
||||
|
left: 'center', // 👈 居中对齐 |
||||
|
textStyle: { |
||||
|
color: '#FFF' |
||||
|
} |
||||
|
}, |
||||
|
series: [ |
||||
|
{ |
||||
|
name: props.title, |
||||
|
type: 'pie', |
||||
|
radius: ['35%', '55%'], // 调整内外半径,增加厚度 |
||||
|
center: ['50%', '40%'], // 👈 调整饼图位置,因为图例已移到顶部 |
||||
|
padAngle: 5, // 添加间隙角度 |
||||
|
avoidLabelOverlap: true, |
||||
|
itemStyle: { |
||||
|
borderRadius: 0, |
||||
|
borderColor: 'transparent', // 取消边框 |
||||
|
borderWidth: 0 // 边框宽度设为0 |
||||
|
}, |
||||
|
label: { |
||||
|
show: true, |
||||
|
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', |
||||
|
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)' // 透明色 |
||||
|
} |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
|
||||
|
chartInstance.setOption(option, true) |
||||
|
} |
||||
|
|
||||
|
// 窗口大小改变时重置图表大小 |
||||
|
const handleResize = () => { |
||||
|
if (chartInstance) { |
||||
|
chartInstance.resize() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 监听数据变化 |
||||
|
watch( |
||||
|
() => props.chartData, |
||||
|
() => { |
||||
|
updateChart() |
||||
|
}, |
||||
|
{ deep: true } |
||||
|
) |
||||
|
|
||||
|
// 初始化和销毁 |
||||
|
onMounted(() => { |
||||
|
initChart() |
||||
|
window.addEventListener('resize', handleResize) |
||||
|
}) |
||||
|
|
||||
|
onBeforeUnmount(() => { |
||||
|
if (chartInstance) { |
||||
|
chartInstance.dispose() |
||||
|
} |
||||
|
window.removeEventListener('resize', handleResize) |
||||
|
}) |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.pie-chart-container { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,254 @@ |
|||||
|
<template> |
||||
|
<div ref="chartContainer" class="wave-line-chart-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 |
||||
|
magnifyMode?: boolean |
||||
|
} |
||||
|
|
||||
|
const props = withDefaults(defineProps<Props>(), { |
||||
|
chartData: () => [], |
||||
|
title: '', |
||||
|
color: '#1296db', |
||||
|
magnifyMode: false |
||||
|
}) |
||||
|
|
||||
|
// 图表容器引用和实例 |
||||
|
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 waveData = props.chartData.map(item => { |
||||
|
return { |
||||
|
...item, |
||||
|
value: item.value |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
const option = { |
||||
|
title: { |
||||
|
// text: props.title, |
||||
|
textStyle: { |
||||
|
color: '#fff', |
||||
|
fontSize: props.magnifyMode ? 24 : 14 |
||||
|
}, |
||||
|
left: 'center' |
||||
|
}, |
||||
|
tooltip: { |
||||
|
trigger: 'axis', |
||||
|
backgroundColor: 'rgba(0, 0, 0, 0.7)', |
||||
|
borderColor: 'rgba(30, 120, 255, 0.4)', |
||||
|
borderWidth: 1, |
||||
|
textStyle: { |
||||
|
color: '#fff', |
||||
|
fontSize: props.magnifyMode ? 20 : 12 |
||||
|
}, |
||||
|
formatter: (params: any) => { |
||||
|
// params是一个数组,包含当前鼠标位置的所有系列数据 |
||||
|
if (params && params.length > 0) { |
||||
|
const time = params[0].name; // 获取时间 |
||||
|
const value = params[0].value; // 获取数据值 |
||||
|
return `时间: ${time}<br/>数值: ${value}`; |
||||
|
} |
||||
|
return ''; |
||||
|
} |
||||
|
}, |
||||
|
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)', |
||||
|
fontSize: props.magnifyMode ? 18 : 10, |
||||
|
rotate: 0, |
||||
|
interval: Math.ceil(props.chartData.length / 12), // 减少间隔以显示更多标签 |
||||
|
formatter: (value: string) => { |
||||
|
// 更灵活的时间格式化 |
||||
|
if (value.includes(':')) { |
||||
|
const parts = value.split(':'); |
||||
|
if (parts.length >= 2) { |
||||
|
// 如果是完整的时间格式 HH:mm:ss |
||||
|
if (parts[2] && parts[2] === '00') { |
||||
|
return parts[0] + ':' + parts[1]; |
||||
|
} |
||||
|
// 如果是 HH:mm 格式 |
||||
|
return parts[0] + ':' + parts[1]; |
||||
|
} |
||||
|
} |
||||
|
return value; |
||||
|
}, |
||||
|
margin: 10 |
||||
|
} |
||||
|
}, |
||||
|
yAxis: { |
||||
|
type: 'value', |
||||
|
// 设置 Y 轴最小值略低于数据中的最小值,以提供更好的可视化效果 |
||||
|
min: (value: any) => { |
||||
|
if (props.chartData && props.chartData.length > 0) { |
||||
|
// 查找数据中的最小值 |
||||
|
const minValue = Math.min(...props.chartData.map(item => item.value)); |
||||
|
// 计算一个合适的最小值,略低于实际最小值 |
||||
|
// const range = value.max - minValue; |
||||
|
return minValue; // 最小值不低于0 |
||||
|
} |
||||
|
return undefined; |
||||
|
}, |
||||
|
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)', |
||||
|
fontSize: props.magnifyMode ? 18 : 10 |
||||
|
} |
||||
|
}, |
||||
|
series: [ |
||||
|
{ |
||||
|
data: waveData.map(item => item.value), |
||||
|
type: 'line', |
||||
|
smooth: true, |
||||
|
showSymbol: true, |
||||
|
symbol: 'circle', |
||||
|
symbolSize: props.magnifyMode ? 8 : 4, |
||||
|
lineStyle: { |
||||
|
color: props.color, |
||||
|
width: props.magnifyMode ? 4 : 2 |
||||
|
}, |
||||
|
areaStyle: { |
||||
|
color: { |
||||
|
type: 'linear', |
||||
|
x: 0, |
||||
|
y: 0, |
||||
|
x2: 0, |
||||
|
y2: 1, |
||||
|
colorStops: [ |
||||
|
{ |
||||
|
offset: 0, |
||||
|
color: props.color + '80' // 添加透明度 |
||||
|
}, |
||||
|
{ |
||||
|
offset: 1, |
||||
|
color: props.color + '00' // 透明 |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
}, |
||||
|
// 添加波浪效果的特殊配置 |
||||
|
emphasis: { |
||||
|
focus: 'series' |
||||
|
}, |
||||
|
// 显示数据点和数值 |
||||
|
label: { |
||||
|
show: true, |
||||
|
position: 'top', |
||||
|
color: '#fff', |
||||
|
fontSize: props.magnifyMode ? 16 : 10, |
||||
|
formatter: '{c}', |
||||
|
distance: props.magnifyMode ? 10 : 5 |
||||
|
} |
||||
|
}, |
||||
|
// 添加一个辅助系列来增强波浪视觉效果 |
||||
|
{ |
||||
|
data: props.chartData.map(item => item.value), |
||||
|
type: 'line', |
||||
|
smooth: true, |
||||
|
showSymbol: false, |
||||
|
lineStyle: { |
||||
|
color: props.color + '60', |
||||
|
width: props.magnifyMode ? 2 : 1, |
||||
|
type: 'dashed' |
||||
|
}, |
||||
|
// 辅助系列不显示在tooltip中 |
||||
|
tooltip: { |
||||
|
show: false |
||||
|
}, |
||||
|
// 辅助系列不显示数据标签以避免重叠 |
||||
|
label: { |
||||
|
show: false |
||||
|
} |
||||
|
} |
||||
|
], |
||||
|
grid: { |
||||
|
left: '1%', |
||||
|
right: '1%', |
||||
|
top: props.title ? (props.magnifyMode ? '10%' : '15%') : '5%', |
||||
|
bottom: '1%', |
||||
|
containLabel: true |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
chartInstance.setOption(option) |
||||
|
} |
||||
|
|
||||
|
// 监听数据变化 |
||||
|
watch( |
||||
|
() => props.chartData, |
||||
|
() => { |
||||
|
updateChart() |
||||
|
}, |
||||
|
{ deep: true } |
||||
|
) |
||||
|
|
||||
|
// 窗口大小改变时重绘图表 |
||||
|
const handleResize = () => { |
||||
|
if (chartInstance) { |
||||
|
chartInstance.resize() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 组件挂载时初始化图表 |
||||
|
onMounted(() => { |
||||
|
// 延迟初始化,确保DOM已经完全渲染 |
||||
|
setTimeout(() => { |
||||
|
initChart() |
||||
|
}, 200) |
||||
|
window.addEventListener('resize', handleResize) |
||||
|
}) |
||||
|
|
||||
|
// 组件销毁前清理资源 |
||||
|
onBeforeUnmount(() => { |
||||
|
window.removeEventListener('resize', handleResize) |
||||
|
if (chartInstance) { |
||||
|
chartInstance.dispose() |
||||
|
chartInstance = null |
||||
|
} |
||||
|
}) |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.wave-line-chart-container { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
min-height: 0; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,120 @@ |
|||||
|
import { MapApi } from "@/api/shorepower/map"; |
||||
|
|
||||
|
export const getOperationTypeLabel = (value: string | number | undefined | null, options: { label: string, value: string | number }[]) => { |
||||
|
try { |
||||
|
const item = options.find(opt => opt.value === value); |
||||
|
return item ? item.label : ''; |
||||
|
} catch (error) { |
||||
|
return '' |
||||
|
} |
||||
|
|
||||
|
}; |
||||
|
|
||||
|
// 装货/卸货
|
||||
|
export const OPERATION_TYPE = [ |
||||
|
{ label: '装货', value: 1 }, |
||||
|
{ label: '卸货', value: 2 }, |
||||
|
{ label: '卸货并装货', value: 2 }, |
||||
|
] |
||||
|
|
||||
|
// 获取类型
|
||||
|
export const CARGO_CATEGORY = [ |
||||
|
{ label: '空船', value: 0 }, |
||||
|
{ label: '散货', value: 1 }, |
||||
|
{ label: '集装箱', value: 2 }, |
||||
|
{ label: '液体货物', value: 3 }, |
||||
|
{ label: '件杂货', value: 4 }, |
||||
|
{ label: '危险品', value: 5 }, |
||||
|
{ label: '冷藏货物', value: 6 }, |
||||
|
] |
||||
|
|
||||
|
// 未使用岸电原因
|
||||
|
export const UNUSED_SHORE_POWER_REASON = [ |
||||
|
{ label: '岸电用电接口不匹配', value: 1 }, |
||||
|
{ label: '岸电设施电压/频率不匹配', value: 2 }, |
||||
|
{ label: '电缆长度不匹配', value: 3 }, |
||||
|
{ label: '气象因素禁止作业', value: 4 }, |
||||
|
{ label: '船电设施损坏', value: 5 }, |
||||
|
{ label: '岸电设施维护中', value: 6 }, |
||||
|
{ label: '无受电设备', value: 7 }, |
||||
|
{ label: '拒绝使用岸电', value: 8 }, |
||||
|
{ label: '其他', value: 9 }, |
||||
|
] |
||||
|
|
||||
|
// 港区map
|
||||
|
export const HARBOR_DISTRICT = async () => { |
||||
|
const res = await MapApi.getHarborDistrictIdAndNameList() |
||||
|
return res.map(item => ({ |
||||
|
...item, |
||||
|
label: item.name, |
||||
|
value: item.id |
||||
|
})) |
||||
|
} |
||||
|
|
||||
|
// doc 码头map
|
||||
|
export const DOCK_DISTRICT = async () => { |
||||
|
const res = await MapApi.getDockIdAndNameList() |
||||
|
return res.map(item => ({ |
||||
|
...item, |
||||
|
label: item.name, |
||||
|
value: item.id |
||||
|
})) |
||||
|
} |
||||
|
|
||||
|
// 岸电状态
|
||||
|
export const SHORE_POWER_STATUS = [ |
||||
|
{ label: '待靠泊', value: 1 }, |
||||
|
{ label: '靠泊中', value: 2 }, |
||||
|
{ label: '岸电接入中', value: 3 }, |
||||
|
{ label: '用电中', value: 4 }, |
||||
|
{ label: '岸电卸载中', value: 5 }, |
||||
|
{ label: '岸电卸载完成', value: 6 }, |
||||
|
{ label: '离泊', value: 7 }, |
||||
|
{ label: '未使用岸电', value: 9 } |
||||
|
] |
||||
|
|
||||
|
export const BERTH_TYPE = [ |
||||
|
{ label: '左舷停舶', value: 'left' }, |
||||
|
{ label: '右舷停舶', value: 'right' }, |
||||
|
] |
||||
|
|
||||
|
// 设施类型
|
||||
|
export const FACILITY_TYPE = [ |
||||
|
{ label: '港区', value: 1 }, |
||||
|
{ label: '码头', value: 2 }, |
||||
|
{ label: '岸电设施', value: 3 }, |
||||
|
{ label: '泊位', value: 4 }, |
||||
|
{ label: '用电接口', value: 5 }, |
||||
|
] |
||||
|
|
||||
|
// 贸易类型
|
||||
|
export const TRADE_TYPE = [ |
||||
|
{ label: '内贸', value: 1 }, |
||||
|
{ label: '外贸', value: 2 }, |
||||
|
] |
||||
|
|
||||
|
// 作业状态
|
||||
|
export const WORK_STATUS = [ |
||||
|
{ label: '待作业', value: 1 }, |
||||
|
{ label: '前往作业中', value: 2 }, |
||||
|
{ label: '接电中', value: 3 }, |
||||
|
{ label: '待拆除岸电', value: 4 }, |
||||
|
{ label: '岸电卸载中', value: 5 }, |
||||
|
{ label: '待上报数据', value: 6 }, |
||||
|
{ label: '作业完成', value: 7 }, |
||||
|
{ label: '未成功接入', value: 9 }, |
||||
|
] |
||||
|
|
||||
|
// 申请状态
|
||||
|
export const APPLY_STATUS = [ |
||||
|
{ label: '待签合同', value: 2 }, |
||||
|
{ label: '待付款', value: 3 }, |
||||
|
{ label: '待确认收款', value: 4 }, |
||||
|
{ label: '待送电', value: 5 }, |
||||
|
{ label: '用电中', value: 6 }, |
||||
|
{ label: '信息收集中', value: 7 }, |
||||
|
{ label: '待退款', value: 8 }, |
||||
|
{ label: '用电完成', value: 9 }, |
||||
|
{ label: '申请完成', value: 10 }, |
||||
|
{ label: '已取消', value: 11 }, |
||||
|
] |
||||
@ -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> |
|
||||
@ -1,529 +0,0 @@ |
|||||
<template> |
|
||||
<div> |
|
||||
<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> --> |
|
||||
<!-- <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')" |
|
||||
style="margin-right: 10px;">视角1</el-button> |
|
||||
<el-button class="view-btn" size="small" type="warning" @click="switchView('view2')" |
|
||||
style="margin-right: 10px;">视角2</el-button> --> |
|
||||
</div> |
|
||||
</div> |
|
||||
|
|
||||
</template> |
|
||||
|
|
||||
<script setup> |
|
||||
import { onMounted, onBeforeUnmount, ref } from 'vue'; |
|
||||
import coordtransform from 'coordtransform'; |
|
||||
import { MapApi } from "@/api/shorepower/map"; |
|
||||
import { toRaw } from 'vue' |
|
||||
// ⚠️ 注意:通过 window 访问全局 Cesium 对象 |
|
||||
const Cesium = window.Cesium; |
|
||||
|
|
||||
let viewer = null; |
|
||||
const cesiumContainerRef = ref(null); // 引用 DOM 元素 |
|
||||
const history = ref(null); // 历史数据 |
|
||||
const dataWithModels = ref([]); // 存储带有模型实例的数据列表 |
|
||||
|
|
||||
// 定义预设视角参数 |
|
||||
const presetViews = { |
|
||||
overview: { |
|
||||
"longitude": 118.5132903711312, |
|
||||
"latitude": 38.84648563549342, |
|
||||
"height": 12675.768680075478, |
|
||||
"heading": 353.6789759103708, |
|
||||
"pitch": -36.657258995301596, |
|
||||
"roll": 0.0343469697692715 |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
const createView = (obj) => { |
|
||||
return { |
|
||||
destination: Cesium.Cartesian3.fromDegrees(obj.longitude, obj.latitude, obj.height), |
|
||||
orientation: { |
|
||||
heading: Cesium.Math.toRadians(obj.heading), |
|
||||
pitch: Cesium.Math.toRadians(obj.pitch), |
|
||||
roll: Cesium.Math.toRadians(obj.roll) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
onMounted(async () => { |
|
||||
if (!Cesium) { |
|
||||
console.error("Cesium 对象未找到,请检查 index.html 文件!"); |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
// 步骤 1: 初始化 Viewer 并配置为使用高德地图影像和平坦地形 |
|
||||
try { |
|
||||
viewer = new Cesium.Viewer(cesiumContainerRef.value, { |
|
||||
// **核心设置:禁用所有默认影像,我们将手动添加高德地图** |
|
||||
imageryProvider: false, |
|
||||
|
|
||||
// **使用平坦地形,避免依赖 Cesium Ion** |
|
||||
terrainProvider: new Cesium.EllipsoidTerrainProvider(), |
|
||||
// sceneMode: Cesium.SceneMode.COLUMBUS_VIEW, |
|
||||
// 禁用所有不必要的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; |
|
||||
|
|
||||
// 启用时钟动画,确保模型能够根据时间更新 |
|
||||
viewer.clock.shouldAnimate = true; |
|
||||
|
|
||||
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}", |
|
||||
subdomains: ['1', '2', '3', '4'], |
|
||||
layer: 'gaodeImgLayer', |
|
||||
style: 'default', |
|
||||
format: 'image/png', |
|
||||
maximumLevel: 16 |
|
||||
}); |
|
||||
const dataInfo = history.value ? history.value : await MapApi.getAllData() |
|
||||
const shipData = await MapApi.getShipInfo({ |
|
||||
harborDistrictId: 1 |
|
||||
}) |
|
||||
console.log('shipData', shipData) |
|
||||
|
|
||||
// const unitedData = |
|
||||
|
|
||||
// 通用添加函数,遍历dataInfo并创建标记 |
|
||||
const addMarkersFromDataInfo = (dataInfo) => { |
|
||||
// 更严格的数组检查 |
|
||||
if (!dataInfo || !Array.isArray(dataInfo) || dataInfo.length === 0) { |
|
||||
console.log('No valid dataInfo array provided'); |
|
||||
return []; |
|
||||
} |
|
||||
|
|
||||
// 创建新数组存储带有模型实例的数据 |
|
||||
const dataWithModelsArray = []; |
|
||||
|
|
||||
console.log(`Processing ${dataInfo.length} items`); |
|
||||
|
|
||||
dataInfo.forEach((item, index) => { |
|
||||
// 检查基本数据结构 |
|
||||
if (!item) { |
|
||||
console.warn(`Item at index ${index} is null or undefined`); |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
try { |
|
||||
// 创建包含模型实例的数据项副本 |
|
||||
let itemWithModel = { ...item }; |
|
||||
|
|
||||
// 解析data字段为JSON对象 |
|
||||
let dataObj; |
|
||||
if (typeof item.data === 'string') { |
|
||||
try { |
|
||||
dataObj = JSON.parse(item.data); |
|
||||
} catch (parseError) { |
|
||||
console.warn(`Failed to parse data for item ${item.id || index}:`, parseError); |
|
||||
dataWithModelsArray.push(itemWithModel); // 添加未解析成功的数据项 |
|
||||
return; |
|
||||
} |
|
||||
} else { |
|
||||
dataObj = item.data; |
|
||||
} |
|
||||
|
|
||||
// 检查dataObj是否有效 |
|
||||
if (!dataObj) { |
|
||||
console.warn(`No data object found for item ${item.id || index}`); |
|
||||
dataWithModelsArray.push(itemWithModel); // 添加无数据对象的数据项 |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
// 获取坐标信息 - 更严格的验证 |
|
||||
let longitude, latitude; |
|
||||
let wgsLon, wgsLat; |
|
||||
if (dataObj.xy && Array.isArray(dataObj.xy) && dataObj.xy.length >= 2) { |
|
||||
const wgsCoords = coordtransform.wgs84togcj02(dataObj.xy[1], dataObj.xy[0]); |
|
||||
wgsLon = wgsCoords[0]; |
|
||||
wgsLat = wgsCoords[1]; |
|
||||
// 确保坐标是有效数字 |
|
||||
latitude = wgsLat; |
|
||||
longitude = wgsLon; |
|
||||
// 检查坐标是否为有效数字 |
|
||||
if (isNaN(latitude) || isNaN(longitude)) { |
|
||||
console.warn(`Invalid coordinates for item ${item.id || index}:`, dataObj.xy); |
|
||||
dataWithModelsArray.push(itemWithModel); // 添加坐标无效的数据项 |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
// 检查坐标范围(简单验证) |
|
||||
if (Math.abs(latitude) > 90 || Math.abs(longitude) > 180) { |
|
||||
console.warn(`Coordinates out of range for item ${item.id || index}:`, dataObj.xy[1], dataObj.xy[0]); |
|
||||
dataWithModelsArray.push(itemWithModel); // 添加坐标超出范围的数据项 |
|
||||
return; |
|
||||
} |
|
||||
} else { |
|
||||
console.warn('无效的坐标信息:', item); |
|
||||
dataWithModelsArray.push(itemWithModel); // 添加无有效坐标的数据项 |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
if (dataObj.icon === 'ship_green') { |
|
||||
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); |
|
||||
const labelPosition = Cesium.Cartesian3.fromDegrees(wgsLon, wgsLat, 35); |
|
||||
// 投放船模型 |
|
||||
const shipModel = viewer.entities.add({ |
|
||||
name: 'Cargo Ship Model', |
|
||||
position: position, |
|
||||
orientation: Cesium.Transforms.headingPitchRollQuaternion( |
|
||||
position, |
|
||||
new Cesium.HeadingPitchRoll( |
|
||||
Cesium.Math.toRadians(dataObj.rotationAngle), // 航向角 (绕Z轴旋转) |
|
||||
Cesium.Math.toRadians(0), // 俯仰角 (绕Y轴旋转) |
|
||||
Cesium.Math.toRadians(0) // 翻滚角 (绕X轴旋转) |
|
||||
) |
|
||||
), |
|
||||
model: { |
|
||||
uri: '/model/cargo_ship_07.glb', |
|
||||
scale: 1.5, // 调整模型大小 |
|
||||
// minimumPixelSize: 100, // 确保模型至少显示为50像素大小 |
|
||||
// 启用深度测试,使模型能够被地形遮挡 |
|
||||
enableDepthTest: true, |
|
||||
// 启用背光剔除,提高渲染性能 |
|
||||
backFaceCulling: true |
|
||||
} |
|
||||
}); |
|
||||
|
|
||||
// 保存模型实例到数据项 |
|
||||
itemWithModel.modelInstance = shipModel; |
|
||||
itemWithModel.modelType = 'ship'; |
|
||||
itemWithModel = { ...itemWithModel, ...itemShipInfo }; |
|
||||
|
|
||||
viewer.entities.add({ |
|
||||
position: labelPosition, // 船上方约10米 |
|
||||
label: { |
|
||||
text: itemShipInfo.shipBasicInfo.name || `Marker-${item.id || index}`, |
|
||||
font: '12px sans-serif', |
|
||||
fillColor: Cesium.Color.WHITE, |
|
||||
outlineColor: Cesium.Color.BLACK, |
|
||||
outlineWidth: 2, |
|
||||
style: Cesium.LabelStyle.FILL_AND_OUTLINE, |
|
||||
pixelOffset: new Cesium.Cartesian2(0, -30), // 偏移量,避免与模型重叠 |
|
||||
disableDepthTestDistance: Number.POSITIVE_INFINITY // 始终可见 |
|
||||
} |
|
||||
}); |
|
||||
const overlayBillboard = viewer.entities.add({ |
|
||||
position: statusPosition, |
|
||||
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.6 + Math.sin(t * 4) * 0.2; // (基准0.6, 幅度±0.2) |
|
||||
return pulse; |
|
||||
}, false) |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
if (dataObj.type === 'text') { |
|
||||
const labelPosition = Cesium.Cartesian3.fromDegrees(wgsLon, wgsLat, 35); |
|
||||
viewer.entities.add({ |
|
||||
position: labelPosition, // 船上方约10米 |
|
||||
label: { |
|
||||
text: dataObj.name || `Marker-${item.id || index}`, |
|
||||
font: '20px sans-serif', |
|
||||
fillColor: Cesium.Color.WHITE, |
|
||||
outlineColor: Cesium.Color.BLACK, |
|
||||
outlineWidth: 2, |
|
||||
style: Cesium.LabelStyle.FILL_AND_OUTLINE, |
|
||||
pixelOffset: new Cesium.Cartesian2(0, -30), // 偏移量,避免与模型重叠 |
|
||||
disableDepthTestDistance: Number.POSITIVE_INFINITY // 始终可见 |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
if (dataObj.type === 'icon' && (dataObj.icon === 'interface_blue' || dataObj.icon === 'interface_red')) { |
|
||||
const position = Cesium.Cartesian3.fromDegrees(wgsLon, wgsLat, 1); |
|
||||
const labelPosition = Cesium.Cartesian3.fromDegrees(wgsLon, wgsLat, 5); |
|
||||
// 投放岸电箱模型 |
|
||||
const electricalBoxModel = viewer.entities.add({ |
|
||||
name: 'Electrical Box Model', |
|
||||
position: position, |
|
||||
orientation: Cesium.Transforms.headingPitchRollQuaternion( |
|
||||
position, |
|
||||
new Cesium.HeadingPitchRoll( |
|
||||
Cesium.Math.toRadians(dataObj.rotationAngle), // 航向角 (绕Z轴旋转) |
|
||||
Cesium.Math.toRadians(0), // 俯仰角 (绕Y轴旋转) |
|
||||
Cesium.Math.toRadians(0) // 翻滚角 (绕X轴旋转) |
|
||||
) |
|
||||
), |
|
||||
model: { |
|
||||
uri: '/model/electrical_box.glb', |
|
||||
scale: 1, // 调整模型大小 |
|
||||
// minimumPixelSize: 10, // 确保模型至少显示为50像素大小 |
|
||||
// 启用深度测试,使模型能够被地形遮挡 |
|
||||
enableDepthTest: true, |
|
||||
// 启用背光剔除,提高渲染性能 |
|
||||
backFaceCulling: true |
|
||||
} |
|
||||
}); |
|
||||
|
|
||||
// 保存模型实例到数据项 |
|
||||
itemWithModel.modelInstance = electricalBoxModel; |
|
||||
itemWithModel.modelType = 'electrical_box'; |
|
||||
|
|
||||
viewer.entities.add({ |
|
||||
position: labelPosition, // 船上方约10米 |
|
||||
label: { |
|
||||
text: '岸电箱' || `Marker-${item.id || index}`, |
|
||||
font: '10px sans-serif', |
|
||||
fillColor: Cesium.Color.WHITE, |
|
||||
outlineColor: Cesium.Color.BLACK, |
|
||||
outlineWidth: 2, |
|
||||
style: Cesium.LabelStyle.FILL_AND_OUTLINE, |
|
||||
pixelOffset: new Cesium.Cartesian2(0, -30), // 偏移量,避免与模型重叠 |
|
||||
disableDepthTestDistance: Number.POSITIVE_INFINITY // 始终可见 |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
// 创建红点标记 |
|
||||
/* const entity = viewer.entities.add({ |
|
||||
name: item.name || `Marker-${item.id || index}`, |
|
||||
position: Cesium.Cartesian3.fromDegrees(wgsLon, wgsLat, 1), |
|
||||
point: { |
|
||||
pixelSize: 10, |
|
||||
color: Cesium.Color.RED, |
|
||||
outlineColor: Cesium.Color.WHITE, |
|
||||
outlineWidth: 2 |
|
||||
}, |
|
||||
// 可以添加标签显示名称 |
|
||||
label: { |
|
||||
text: item.name || `Marker-${item.id || index}`, |
|
||||
font: '14px sans-serif', |
|
||||
fillColor: Cesium.Color.WHITE, |
|
||||
outlineColor: Cesium.Color.BLACK, |
|
||||
outlineWidth: 2, |
|
||||
style: Cesium.LabelStyle.FILL_AND_OUTLINE, |
|
||||
verticalOrigin: Cesium.VerticalOrigin.BOTTOM, |
|
||||
pixelOffset: new Cesium.Cartesian2(0, -15) |
|
||||
} |
|
||||
}); |
|
||||
// 保存点标记实例到数据项 |
|
||||
itemWithModel.modelInstance = entity; |
|
||||
itemWithModel.modelType = 'point'; |
|
||||
*/ |
|
||||
|
|
||||
// 将处理后的数据项添加到新数组 |
|
||||
dataWithModelsArray.push(itemWithModel); |
|
||||
} catch (error) { |
|
||||
console.error('Error processing item:', item, error); |
|
||||
// 创建一个基本的数据项副本并添加到数组 |
|
||||
dataWithModelsArray.push({ ...item }); |
|
||||
} |
|
||||
}); |
|
||||
|
|
||||
return dataWithModelsArray; |
|
||||
}; |
|
||||
|
|
||||
// 步骤 3: 将图层添加到 Viewer 中 |
|
||||
viewer.imageryLayers.addImageryProvider(gaodeImage); |
|
||||
|
|
||||
// 调用通用函数创建标记并获取带有模型实例的数据 |
|
||||
dataWithModels.value = addMarkersFromDataInfo(dataInfo); |
|
||||
|
|
||||
// 设置初始视角 |
|
||||
viewer.camera.flyTo(createView(presetViews.overview)); |
|
||||
// 设置显示高度阈值 |
|
||||
const SHOW_HEIGHT = 30000; // 高度 > 5000m 才显示模型等信息 |
|
||||
// const targets = []; // 把需要控制的 entities push 进来 |
|
||||
const targets = dataWithModels.value |
|
||||
.filter(item => item.modelInstance) |
|
||||
.map(item => toRaw(item.modelInstance)); |
|
||||
console.log('targets', targets) |
|
||||
viewer.camera.changed.addEventListener(() => { |
|
||||
// 这个函数会在相机的任何属性发生变化后立即被调用。 |
|
||||
const height = viewer.camera.positionCartographic.height; |
|
||||
// console.log("相机高度(即缩放级别)已变化:", height.toFixed(2), "米"); |
|
||||
|
|
||||
// 控制模型显示/隐藏 |
|
||||
const visible = height <= SHOW_HEIGHT; // 1000m内才显示模型等信息 |
|
||||
targets.forEach(entity => { |
|
||||
if (entity.show !== visible) { |
|
||||
entity.show = visible; |
|
||||
} |
|
||||
}); |
|
||||
|
|
||||
// 其他缩放级别逻辑 |
|
||||
if (height < 100000) { |
|
||||
// 放大到街景级别 |
|
||||
console.log("当前处于近距离缩放级别。"); |
|
||||
} else if (height > 5000000) { |
|
||||
// 缩小到全球级别 |
|
||||
console.log("当前处于远距离缩放级别。"); |
|
||||
} |
|
||||
}); |
|
||||
|
|
||||
// 添加地图点击事件 |
|
||||
const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas); |
|
||||
handler.setInputAction(function (movement) { |
|
||||
console.log('地图点击事件触发'); |
|
||||
|
|
||||
// 尝试多种方式获取点击位置 |
|
||||
const ray = viewer.camera.getPickRay(movement.position); |
|
||||
|
|
||||
// 方法1: 从地球表面拾取 |
|
||||
let cartesian = viewer.scene.globe.pick(ray, viewer.scene); |
|
||||
|
|
||||
// 如果从地球表面拾取失败,尝试从场景中拾取 |
|
||||
if (!cartesian) { |
|
||||
cartesian = viewer.scene.pickPosition(movement.position); |
|
||||
console.log('使用场景拾取方法'); |
|
||||
} |
|
||||
|
|
||||
// 如果仍然失败,尝试使用相机计算位置 |
|
||||
if (!cartesian) { |
|
||||
// 获取屏幕中心点作为替代方案(仅用于调试) |
|
||||
console.log('无法从点击位置获取精确坐标'); |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
if (cartesian) { |
|
||||
const cartographic = Cesium.Cartographic.fromCartesian(cartesian); |
|
||||
if (cartographic) { |
|
||||
const longitude = Cesium.Math.toDegrees(cartographic.longitude); |
|
||||
const latitude = Cesium.Math.toDegrees(cartographic.latitude); |
|
||||
const gcj02 = coordtransform.gcj02towgs84(longitude, latitude); |
|
||||
console.log('点击位置经纬度:', { |
|
||||
longitude: gcj02[0], |
|
||||
latitude: gcj02[1] |
|
||||
}); |
|
||||
} else { |
|
||||
console.log('无法从笛卡尔坐标转换为大地坐标'); |
|
||||
} |
|
||||
} else { |
|
||||
console.log('未能获取到地球表面的点击位置'); |
|
||||
} |
|
||||
}, Cesium.ScreenSpaceEventType.LEFT_CLICK); |
|
||||
|
|
||||
// 确保启用场景的鼠标事件 |
|
||||
viewer.scene.screenSpaceCameraController.enableInputs = true; |
|
||||
|
|
||||
// 启用拾取透明物体 |
|
||||
viewer.scene.useDepthPicking = true; |
|
||||
|
|
||||
console.log('Cesium 高德地图 Viewer 初始化成功'); |
|
||||
} catch (error) { |
|
||||
console.error('Cesium Viewer 初始化失败:', error); |
|
||||
} |
|
||||
}); |
|
||||
|
|
||||
// 视角切换方法 |
|
||||
const switchView = (viewName) => { |
|
||||
const viewParams = presetViews[viewName]; |
|
||||
if (viewParams && viewer) { |
|
||||
viewer.camera.flyTo({ |
|
||||
...createView(viewParams), |
|
||||
duration: 2.0 // 动画持续时间(秒) |
|
||||
}); |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
// 视角切换方法 |
|
||||
const switchModelView = (view) => { |
|
||||
const modelInstance = toRaw(view); |
|
||||
console.log(modelInstance) |
|
||||
if (viewer && view) { |
|
||||
viewer.flyTo(modelInstance); |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
// 获取当前视角信息的方法 |
|
||||
const getCurrentViewInfo = () => { |
|
||||
if (!viewer) { |
|
||||
console.warn('Viewer 尚未初始化'); |
|
||||
return null; |
|
||||
} |
|
||||
const camera = viewer.camera; |
|
||||
|
|
||||
const position = camera.position; // 世界笛卡尔坐标系 x,y,z |
|
||||
const cartographic = Cesium.Cartographic.fromCartesian(position); |
|
||||
console.log( |
|
||||
{ |
|
||||
longitude: Cesium.Math.toDegrees(cartographic.longitude), |
|
||||
latitude: Cesium.Math.toDegrees(cartographic.latitude), |
|
||||
height: cartographic.height, |
|
||||
heading: Cesium.Math.toDegrees(camera.heading), |
|
||||
pitch: Cesium.Math.toDegrees(camera.pitch), |
|
||||
roll: Cesium.Math.toDegrees(camera.roll) |
|
||||
} |
|
||||
) |
|
||||
|
|
||||
}; |
|
||||
|
|
||||
// 暴露方法和数据给父组件使用 |
|
||||
defineExpose({ |
|
||||
switchView, |
|
||||
dataWithModels, |
|
||||
switchModelView |
|
||||
}); |
|
||||
|
|
||||
onBeforeUnmount(() => { |
|
||||
// 销毁 Viewer,释放 WebGL 资源 |
|
||||
if (viewer) { |
|
||||
viewer.destroy(); |
|
||||
viewer = null; |
|
||||
console.log('Cesium Viewer 已销毁'); |
|
||||
} |
|
||||
}); |
|
||||
</script> |
|
||||
|
|
||||
<style scoped> |
|
||||
.cesium-container { |
|
||||
width: 100%; |
|
||||
height: 100vh; |
|
||||
margin: 0; |
|
||||
padding: 0; |
|
||||
overflow: hidden; |
|
||||
} |
|
||||
|
|
||||
.btn-group { |
|
||||
position: absolute; |
|
||||
top: 10px; |
|
||||
z-index: 1000; |
|
||||
display: flex; |
|
||||
gap: 12rpx; |
|
||||
} |
|
||||
</style> |
|
||||
@ -0,0 +1,244 @@ |
|||||
|
import { RealtimeDeviceData, ShipRespVo } from "@/types/shorepower"; |
||||
|
import dayjs from "dayjs"; |
||||
|
import { MapApi } from "@/api/shorepower/map"; |
||||
|
/** |
||||
|
* 将毫秒时间戳格式化为 'YYYY/MM/DD HH:mm:ss' 格式 |
||||
|
* @param timestamp 毫秒时间戳 |
||||
|
* @returns 格式化后的时间字符串 |
||||
|
*/ |
||||
|
export const formatTimestamp = ( |
||||
|
timestamp: number | null | undefined, |
||||
|
format = 'YYYY/MM/DD HH:mm:ss' |
||||
|
): string => { |
||||
|
if (!timestamp) return ''; |
||||
|
// 处理秒级时间戳(10位数字)
|
||||
|
if (timestamp < 1e12) { |
||||
|
timestamp *= 1000; |
||||
|
} |
||||
|
return dayjs(timestamp).format(format); |
||||
|
}; |
||||
|
|
||||
|
/** |
||||
|
* 将两个时间戳之间的差值格式化为 "X小时Y分钟Z秒" |
||||
|
* @param {number} startTime - 开始时间戳(毫秒) |
||||
|
* @param {number} endTime - 结束时间戳(毫秒) |
||||
|
* @returns {string} 格式化后的时间字符串,如 "2小时30分钟15秒" |
||||
|
*/ |
||||
|
export function formatDuration(startTime?: number, endTime?: number): string { |
||||
|
if (!startTime || !endTime) return '--'; |
||||
|
const diffMs = Math.max(0, endTime - startTime); |
||||
|
const totalSeconds = Math.floor(diffMs / 1000); |
||||
|
|
||||
|
const hours = Math.floor(totalSeconds / 3600); |
||||
|
const minutes = Math.floor((totalSeconds % 3600) / 60); |
||||
|
const seconds = totalSeconds % 60; |
||||
|
|
||||
|
let result = ''; |
||||
|
if (hours > 0) result += `${hours}小时`; |
||||
|
if (minutes > 0) result += `${minutes}分钟`; |
||||
|
if (seconds > 0 || result === '') result += `${seconds}秒`; |
||||
|
|
||||
|
return result || '0秒'; |
||||
|
} |
||||
|
|
||||
|
// 岸电使用文本构建
|
||||
|
export function showStatus(ship: ShipRespVo, realtimeDeviceData?: RealtimeDeviceData[]): { statusClass: string, status: string, useTime: string, useValue: string } | null { |
||||
|
const { usageRecordInfo, applyInfo } = ship; |
||||
|
if (!applyInfo || !usageRecordInfo || !usageRecordInfo.beginTime) { |
||||
|
return null; |
||||
|
} |
||||
|
if (applyInfo.reason == 0 && usageRecordInfo && usageRecordInfo.beginTime) { |
||||
|
|
||||
|
const start = new Date(usageRecordInfo.beginTime); |
||||
|
const end = usageRecordInfo.endTime ? new Date(usageRecordInfo.endTime) : new Date(); |
||||
|
|
||||
|
// 校验日期有效性
|
||||
|
if (isNaN(start.getTime()) || isNaN(end.getTime())) { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
// 校验时间顺序
|
||||
|
if (end < start) { |
||||
|
return null; // 或抛出错误、记录日志等
|
||||
|
} |
||||
|
|
||||
|
// 计算总毫秒差
|
||||
|
const diffMs = end.getTime() - start.getTime(); |
||||
|
|
||||
|
// 转换为秒
|
||||
|
const totalSeconds = Math.floor(diffMs / 1000); |
||||
|
|
||||
|
const hours = Math.floor(totalSeconds / 3600); |
||||
|
const minutes = Math.floor((totalSeconds % 3600) / 60); |
||||
|
const seconds = totalSeconds % 60; |
||||
|
|
||||
|
let useValue = 0; |
||||
|
|
||||
|
// 检查是否有结束读数
|
||||
|
const hasEndReading = usageRecordInfo.powerEndManualReading || usageRecordInfo.powerEndSystemReading; |
||||
|
|
||||
|
if (hasEndReading) { |
||||
|
// 有结束读数,使用常规计算方式
|
||||
|
// 优先计算人工差值
|
||||
|
const manualDiff = usageRecordInfo.powerStartManualReading !== undefined && usageRecordInfo.powerEndManualReading !== undefined |
||||
|
? usageRecordInfo.powerEndManualReading - usageRecordInfo.powerStartManualReading |
||||
|
: null; |
||||
|
// 然后计算系统差值
|
||||
|
const systemDiff = usageRecordInfo.powerStartSystemReading !== undefined && usageRecordInfo.powerEndSystemReading !== undefined |
||||
|
? usageRecordInfo.powerEndSystemReading - usageRecordInfo.powerStartSystemReading |
||||
|
: null; |
||||
|
// 使用人工差值优先,然后系统差值,最后默认0
|
||||
|
useValue = manualDiff ?? systemDiff ?? 0; |
||||
|
} else { |
||||
|
// 没有结束读数,使用实时数据计算
|
||||
|
const deviceId = ship.shorePower?.totalPowerDeviceId; |
||||
|
if (deviceId) { |
||||
|
try { |
||||
|
// 调用API获取实时数据
|
||||
|
const measureValueStr = getValueById(realtimeDeviceData || [], deviceId, |
||||
|
'measureValue') |
||||
|
if (measureValueStr !== undefined) { |
||||
|
|
||||
|
// 优先使用人工开始读数,然后系统开始读数
|
||||
|
const startReading = usageRecordInfo.powerStartManualReading ?? usageRecordInfo.powerStartSystemReading ?? 0; |
||||
|
const measureValue = typeof measureValueStr === 'string' ? parseFloat(measureValueStr) : measureValueStr; |
||||
|
useValue = measureValue - startReading; |
||||
|
} |
||||
|
} catch (error) { |
||||
|
console.error('获取实时数据失败:', error); |
||||
|
useValue = 0; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
statusClass: 'status-using', |
||||
|
status: `使用岸电${hours}小时${minutes}分钟${seconds}秒,${useValue.toFixed(2)}kWH`, |
||||
|
useTime: `${hours}小时${minutes}分钟${seconds}秒`, |
||||
|
useValue: useValue.toFixed(2) + 'kWH' |
||||
|
} |
||||
|
} else if (applyInfo.reason != 0) { |
||||
|
// 状态码映射表 - 包含状态文本和颜色类型
|
||||
|
const statusMap: Record<string, { text: string; colorType: string }> = { |
||||
|
'-1': { text: '未知错误', colorType: 'default' }, |
||||
|
'1': { text: '岸电用电接口不匹配', colorType: 'default' }, |
||||
|
'2': { text: '岸电设施电压/频率不匹配', colorType: 'success' }, |
||||
|
'3': { text: '电缆长度不匹配', colorType: 'success' }, |
||||
|
'4': { text: '气象因素禁止作业', colorType: 'success' }, |
||||
|
'5': { text: '船电设施损坏', colorType: 'warning' }, |
||||
|
'6': { text: '岸电设施维护中', colorType: 'warning' }, |
||||
|
'7': { text: '无受电设备', colorType: 'danger' }, |
||||
|
'8': { text: '拒绝使用岸电', colorType: 'danger' }, |
||||
|
'9': { text: '其他', colorType: 'danger' } |
||||
|
} |
||||
|
|
||||
|
const reasonKey = applyInfo.reason?.toString() || '-1' |
||||
|
const statusInfo = statusMap[reasonKey] || { text: applyInfo.reason, colorType: 'default' } |
||||
|
|
||||
|
// 根据颜色类型设置对应的状态类
|
||||
|
let statusClass = 'status-cable' |
||||
|
switch (statusInfo.colorType) { |
||||
|
case 'danger': |
||||
|
statusClass = 'status-danger' |
||||
|
break |
||||
|
case 'warning': |
||||
|
statusClass = 'status-warning' |
||||
|
break |
||||
|
case 'success': |
||||
|
statusClass = 'status-success' |
||||
|
break |
||||
|
default: |
||||
|
statusClass = 'status-cable' |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
statusClass: statusClass, |
||||
|
status: statusInfo.text |
||||
|
} |
||||
|
} else { |
||||
|
return { |
||||
|
statusClass: '', |
||||
|
status: 'unknown' |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
/** |
||||
|
* 根据 id 查找对象数组中的指定字段值,并保留两位小数 |
||||
|
* @param list - 对象数组,每个对象必须包含 id 字段 |
||||
|
* @param id - 要查找的 id 值 |
||||
|
* @param valueField - 要返回的字段名(必须是对象中除 id 外的 key) |
||||
|
* @returns 字段值,未找到则返回 undefined |
||||
|
*/ |
||||
|
export function getValueById<T extends { id: number | string }, K extends keyof Omit<T, 'id'>>( |
||||
|
list: T[], |
||||
|
id: T['id'], |
||||
|
valueField: K |
||||
|
): T[K] extends number | string ? string | undefined : T[K] | undefined { |
||||
|
const item = list.find(item => item.id === id); |
||||
|
|
||||
|
if (!item) return undefined; |
||||
|
|
||||
|
const value = item[valueField]; |
||||
|
|
||||
|
// 如果值是数字或可以转换为数字的字符串,则保留两位小数
|
||||
|
if (typeof value === 'number') { |
||||
|
return Number(value).toFixed(2) as any; |
||||
|
} |
||||
|
|
||||
|
if (typeof value === 'string' && !isNaN(Number(value))) { |
||||
|
return Number(value).toFixed(2) as any; |
||||
|
} |
||||
|
|
||||
|
// 否则返回原始值
|
||||
|
return value as any; |
||||
|
} |
||||
|
|
||||
|
export function parseRangeToTimestamp(range: string[], granularity: 'year' | 'month' | 'week' | 'day'): [number, number] | null { |
||||
|
if (!range || range.length !== 2) return null; |
||||
|
|
||||
|
const [startStr, endStr] = range; |
||||
|
|
||||
|
let startDate: Date; |
||||
|
let endDate: Date; |
||||
|
|
||||
|
switch (granularity) { |
||||
|
case 'year': |
||||
|
// '2023' → 2023-01-01 00:00:00 ~ 2023-12-31 23:59:59
|
||||
|
startDate = new Date(`${startStr}-01-01T00:00:00`); |
||||
|
endDate = new Date(`${endStr}-12-31T23:59:59`); |
||||
|
break; |
||||
|
|
||||
|
case 'month': |
||||
|
// '2023-05' → 2023-05-01 00:00:00 ~ 2023-05-31 23:59:59
|
||||
|
startDate = new Date(`${startStr}-01T00:00:00`); |
||||
|
// 获取该月最后一天
|
||||
|
const yearMonth = startStr.split('-'); |
||||
|
const nextMonth = new Date(+yearMonth[0], +yearMonth[1], 0); // 本月最后一天
|
||||
|
endDate = new Date(nextMonth.getFullYear(), nextMonth.getMonth(), nextMonth.getDate(), 23, 59, 59); |
||||
|
// 同样处理 endStr
|
||||
|
const endYearMonth = endStr.split('-'); |
||||
|
const endNextMonth = new Date(+endYearMonth[0], +endYearMonth[1], 0); |
||||
|
endDate = new Date(endNextMonth.getFullYear(), endNextMonth.getMonth(), endNextMonth.getDate(), 23, 59, 59); |
||||
|
break; |
||||
|
|
||||
|
case 'week': |
||||
|
case 'day': |
||||
|
default: |
||||
|
// '2023-05-01' → 2023-05-01 00:00:00 ~ 2023-05-01 23:59:59(week 实际也是日期)
|
||||
|
startDate = new Date(`${startStr}T00:00:00`); |
||||
|
endDate = new Date(`${endStr}T23:59:59`); |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
const start = startDate.getTime(); |
||||
|
const end = endDate.getTime(); |
||||
|
|
||||
|
if (isNaN(start) || isNaN(end)) { |
||||
|
console.error('日期解析失败:', range, granularity); |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
return [start, end]; |
||||
|
} |
||||
File diff suppressed because it is too large
@ -0,0 +1,479 @@ |
|||||
|
interface ShorePowerBerth { |
||||
|
id: number |
||||
|
berthId: number |
||||
|
equipmentId: number |
||||
|
name: string |
||||
|
totalPowerDeviceId: number |
||||
|
frequencyDeviceId: number |
||||
|
voltageDeviceId: number |
||||
|
currentDeviceId: number |
||||
|
status: number |
||||
|
createTime: number // Unix timestamp in milliseconds
|
||||
|
shorePowerEquipmentInfo: ShorePowerEquipmentInfo |
||||
|
storePowerStatus?: string |
||||
|
} |
||||
|
|
||||
|
interface ShorePowerEquipmentInfo { |
||||
|
id: number |
||||
|
dockId: number |
||||
|
name: string |
||||
|
capacity: string // 可考虑转为 number,但原始数据为字符串 "2000"
|
||||
|
voltage: string // 如 "0.4/0.44"
|
||||
|
frequency: string // 如 "50/60"
|
||||
|
createTime: number // Unix timestamp in milliseconds
|
||||
|
} |
||||
|
|
||||
|
interface shipHistoryPageRequest { |
||||
|
/** |
||||
|
* 查询编号 |
||||
|
*/ |
||||
|
ids?: number[] |
||||
|
/** |
||||
|
* 页码,从 1 开始 |
||||
|
*/ |
||||
|
pageNo: number |
||||
|
/** |
||||
|
* 每页条数,最大值为 100 |
||||
|
*/ |
||||
|
pageSize: number |
||||
|
/** |
||||
|
* 船舶编号 |
||||
|
*/ |
||||
|
shipId?: number |
||||
|
/** |
||||
|
* 查询类型 |
||||
|
*/ |
||||
|
type?: number |
||||
|
[property: string]: any |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* CommonResultPageResultShipRespVo |
||||
|
*/ |
||||
|
interface apiResponse<T> { |
||||
|
code?: number |
||||
|
data?: T |
||||
|
msg?: string |
||||
|
[property: string]: any |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* PageResultShipRespVo,分页结果 |
||||
|
*/ |
||||
|
interface PageResultShipRespVo<T> { |
||||
|
/** |
||||
|
* 数据 |
||||
|
*/ |
||||
|
list: T[] |
||||
|
/** |
||||
|
* 总量 |
||||
|
*/ |
||||
|
total: number |
||||
|
[property: string]: any |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* ShipRespVo,管理后台 - 船舶岸电使用情况 Response VO |
||||
|
*/ |
||||
|
export interface ShipRespVo { |
||||
|
/** |
||||
|
* 船舶状态 |
||||
|
*/ |
||||
|
shipStatus?: string |
||||
|
/** |
||||
|
* 用电申请信息 |
||||
|
*/ |
||||
|
applyInfo: ApplyRespVO |
||||
|
/** |
||||
|
* 船舶基础信息 |
||||
|
*/ |
||||
|
shipBasicInfo: ShipBasicInfoRespVO |
||||
|
/** |
||||
|
* 用电接口信息 |
||||
|
*/ |
||||
|
shorePower: ShorePowerRespVO |
||||
|
/** |
||||
|
* 岸电接口与船舶关系 |
||||
|
*/ |
||||
|
shorePowerAndShip: ShorePowerAndShipRespVO |
||||
|
/** |
||||
|
* 岸电设施信息 |
||||
|
*/ |
||||
|
shorePowerEquipment: ShorePowerEquipmentRespVO |
||||
|
/** |
||||
|
* 船舶用电信息 |
||||
|
*/ |
||||
|
usageRecordInfo: UsageRecordRespVO |
||||
|
[property: string]: any |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 用电申请信息 |
||||
|
* |
||||
|
* ApplyRespVO,管理后台 - 用电申请 Response VO |
||||
|
*/ |
||||
|
export interface ApplyRespVO { |
||||
|
/** |
||||
|
* 船方代理单位 |
||||
|
*/ |
||||
|
agentCompany: string |
||||
|
/** |
||||
|
* 代理人联系方式 |
||||
|
*/ |
||||
|
agentContact: string |
||||
|
/** |
||||
|
* 是否申请使用岸电 |
||||
|
*/ |
||||
|
applyShorePower: boolean |
||||
|
/** |
||||
|
* 到达泊位 |
||||
|
*/ |
||||
|
arrivalBerth: number |
||||
|
/** |
||||
|
* 到达码头 |
||||
|
*/ |
||||
|
arrivalDock: number |
||||
|
/** |
||||
|
* 到达港 |
||||
|
*/ |
||||
|
arrivalHarborDistrict: number |
||||
|
/** |
||||
|
* 创建时间 |
||||
|
*/ |
||||
|
createTime: Date |
||||
|
/** |
||||
|
* 起运港 |
||||
|
*/ |
||||
|
departureHarborDistrict: string |
||||
|
/** |
||||
|
* 编号 |
||||
|
*/ |
||||
|
id: number |
||||
|
/** |
||||
|
* 货物类型(装货) |
||||
|
*/ |
||||
|
loadingCargoCategory: number |
||||
|
/** |
||||
|
* 货物名称(装货) |
||||
|
*/ |
||||
|
loadingCargoName: string |
||||
|
/** |
||||
|
* 货物吨数(装货) |
||||
|
*/ |
||||
|
loadingCargoTonnage: number |
||||
|
/** |
||||
|
* 装货/卸货 |
||||
|
*/ |
||||
|
operationType: number |
||||
|
/** |
||||
|
* 计划靠泊时间 |
||||
|
*/ |
||||
|
plannedBerthTime: Date |
||||
|
/** |
||||
|
* 计划离泊时间 |
||||
|
*/ |
||||
|
plannedDepartureTime: Date |
||||
|
/** |
||||
|
* 未使用岸电原因 |
||||
|
*/ |
||||
|
reason?: number |
||||
|
/** |
||||
|
* 备注 |
||||
|
*/ |
||||
|
remark?: string |
||||
|
/** |
||||
|
* 船舶编号 |
||||
|
*/ |
||||
|
shipId: number |
||||
|
/** |
||||
|
* 申请状态 |
||||
|
*/ |
||||
|
status: number |
||||
|
/** |
||||
|
* 内贸/外贸 |
||||
|
*/ |
||||
|
tradeType: number |
||||
|
/** |
||||
|
* 货物类型(卸货) |
||||
|
*/ |
||||
|
unloadingCargoCategory: number |
||||
|
/** |
||||
|
* 货物名称(卸货) |
||||
|
*/ |
||||
|
unloadingCargoName: string |
||||
|
/** |
||||
|
* 货物吨数(卸货) |
||||
|
*/ |
||||
|
unloadingCargoTonnage: number |
||||
|
/** |
||||
|
* 航次 |
||||
|
*/ |
||||
|
voyage: string |
||||
|
[property: string]: any |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 船舶基础信息 |
||||
|
* |
||||
|
* ShipBasicInfoRespVO,管理后台 - 船舶基础信息 Response VO |
||||
|
*/ |
||||
|
export interface ShipBasicInfoRespVO { |
||||
|
/** |
||||
|
* 船舶呼号 |
||||
|
*/ |
||||
|
callSign: string |
||||
|
/** |
||||
|
* 创建时间 |
||||
|
*/ |
||||
|
createTime: Date |
||||
|
/** |
||||
|
* 满载吃水深度 |
||||
|
*/ |
||||
|
fullLoadDraft: number |
||||
|
/** |
||||
|
* 编号 |
||||
|
*/ |
||||
|
id: number |
||||
|
/** |
||||
|
* 船检登记号 |
||||
|
*/ |
||||
|
inspectionNo: string |
||||
|
/** |
||||
|
* 船舶长度 |
||||
|
*/ |
||||
|
length: number |
||||
|
/** |
||||
|
* 船名 |
||||
|
*/ |
||||
|
name: string |
||||
|
/** |
||||
|
* 英文船名 |
||||
|
*/ |
||||
|
nameEn: string |
||||
|
/** |
||||
|
* 航运单位名称 |
||||
|
*/ |
||||
|
shippingCompany: string |
||||
|
/** |
||||
|
* 岸电负责人 |
||||
|
*/ |
||||
|
shorePowerContact: string |
||||
|
/** |
||||
|
* 联系方式 |
||||
|
*/ |
||||
|
shorePowerContactPhone: string |
||||
|
/** |
||||
|
* 船舶吨位 |
||||
|
*/ |
||||
|
tonnage: number |
||||
|
/** |
||||
|
* 船舶宽度 |
||||
|
*/ |
||||
|
width: number |
||||
|
[property: string]: any |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 用电接口信息 |
||||
|
* |
||||
|
* ShorePowerRespVO,管理后台 - 岸电 Response VO |
||||
|
*/ |
||||
|
export interface ShorePowerRespVO { |
||||
|
/** |
||||
|
* 泊位编号 |
||||
|
*/ |
||||
|
berthId: number |
||||
|
/** |
||||
|
* 创建时间 |
||||
|
*/ |
||||
|
createTime: Date |
||||
|
/** |
||||
|
* 岸电设备编号 |
||||
|
*/ |
||||
|
equipmentId: number |
||||
|
/** |
||||
|
* 频率设备编号 |
||||
|
*/ |
||||
|
frequencyDeviceId: number |
||||
|
/** |
||||
|
* 编号 |
||||
|
*/ |
||||
|
id: number |
||||
|
/** |
||||
|
* 名称 |
||||
|
*/ |
||||
|
name: string |
||||
|
/** |
||||
|
* 总电量设备编号 |
||||
|
*/ |
||||
|
totalPowerDeviceId: number |
||||
|
/** |
||||
|
* 电压设备编号 |
||||
|
*/ |
||||
|
voltageDeviceId: number |
||||
|
[property: string]: any |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 岸电接口与船舶关系 |
||||
|
* |
||||
|
* ShorePowerAndShipRespVO,管理后台 - 岸电接口与船舶关系 Response VO |
||||
|
*/ |
||||
|
export interface ShorePowerAndShipRespVO { |
||||
|
/** |
||||
|
* 申请单编号 |
||||
|
*/ |
||||
|
applyId: number |
||||
|
/** |
||||
|
* 创建时间 |
||||
|
*/ |
||||
|
createTime: Date |
||||
|
/** |
||||
|
* 编号 |
||||
|
*/ |
||||
|
id: number |
||||
|
/** |
||||
|
* 船舶编号 |
||||
|
*/ |
||||
|
shipId: number |
||||
|
/** |
||||
|
* 岸电接口编号 |
||||
|
*/ |
||||
|
shorePowerId: number |
||||
|
/** |
||||
|
* 靠泊状态 |
||||
|
*/ |
||||
|
status: number |
||||
|
/** |
||||
|
* 靠泊类型 |
||||
|
*/ |
||||
|
type: string |
||||
|
[property: string]: any |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 岸电设施信息 |
||||
|
* |
||||
|
* ShorePowerEquipmentRespVO,管理后台 - 岸电设施 Response VO |
||||
|
*/ |
||||
|
export interface ShorePowerEquipmentRespVO { |
||||
|
/** |
||||
|
* 装机容量 |
||||
|
*/ |
||||
|
capacity: string |
||||
|
/** |
||||
|
* 创建时间 |
||||
|
*/ |
||||
|
createTime: Date |
||||
|
/** |
||||
|
* 码头编号 |
||||
|
*/ |
||||
|
dockId: number |
||||
|
/** |
||||
|
* 频率 |
||||
|
*/ |
||||
|
frequency: string |
||||
|
/** |
||||
|
* 编号 |
||||
|
*/ |
||||
|
id: number |
||||
|
/** |
||||
|
* 名称 |
||||
|
*/ |
||||
|
name: string |
||||
|
/** |
||||
|
* 电压 |
||||
|
*/ |
||||
|
voltage: string |
||||
|
[property: string]: any |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 船舶用电信息 |
||||
|
* |
||||
|
* UsageRecordRespVO,管理后台 - 用电记录 Response VO |
||||
|
*/ |
||||
|
export interface UsageRecordRespVO { |
||||
|
/** |
||||
|
* 实际靠泊时间 |
||||
|
*/ |
||||
|
actualBerthTime?: Date |
||||
|
/** |
||||
|
* 实际离泊时间 |
||||
|
*/ |
||||
|
actualDepartureTime?: number | null | undefined |
||||
|
/** |
||||
|
* 用电申请编号 |
||||
|
*/ |
||||
|
applyId: number |
||||
|
/** |
||||
|
* 用电开始时供电方操作人 |
||||
|
*/ |
||||
|
beginPowerSupplyOperator?: string |
||||
|
/** |
||||
|
* 用电开始时用电方操作人 |
||||
|
*/ |
||||
|
beginPowerUsageOperator?: string |
||||
|
/** |
||||
|
* 用电开始时间 |
||||
|
*/ |
||||
|
beginTime?: number |
||||
|
/** |
||||
|
* 创建时间 |
||||
|
*/ |
||||
|
createTime: Date |
||||
|
/** |
||||
|
* 用电结束时间 |
||||
|
*/ |
||||
|
endTime?: number |
||||
|
/** |
||||
|
* 编号 |
||||
|
*/ |
||||
|
id: number |
||||
|
/** |
||||
|
* 用电结束时供电方操作人 |
||||
|
*/ |
||||
|
overPowerSupplyOperator?: string |
||||
|
/** |
||||
|
* 用电结束时用电方操作人 |
||||
|
*/ |
||||
|
overPowerUsageOperator?: string |
||||
|
/** |
||||
|
* 用电结束时间人工读数 |
||||
|
*/ |
||||
|
powerEndManualReading?: number |
||||
|
/** |
||||
|
* 用电结束时间系统读数 |
||||
|
*/ |
||||
|
powerEndSystemReading?: number |
||||
|
/** |
||||
|
* 用电开始时间人工读数 |
||||
|
*/ |
||||
|
powerStartManualReading?: number |
||||
|
/** |
||||
|
* 用电开始时间系统读数 |
||||
|
*/ |
||||
|
powerStartSystemReading?: number |
||||
|
/** |
||||
|
* 船舶编号 |
||||
|
*/ |
||||
|
shipId: number |
||||
|
/** |
||||
|
* 作业单状态 |
||||
|
*/ |
||||
|
status: number |
||||
|
[property: string]: any |
||||
|
} |
||||
|
|
||||
|
interface RealtimeDeviceData { |
||||
|
createTime: number // 时间戳(毫秒)
|
||||
|
deviceCode: string |
||||
|
deviceId: number |
||||
|
deviceName: string |
||||
|
deviceStatus: number |
||||
|
id: number |
||||
|
incrementCost: number |
||||
|
incrementValue: number |
||||
|
measureTime: number // 时间戳(毫秒)
|
||||
|
measureValue: number |
||||
|
} |
||||
Loading…
Reference in new issue