Browse Source

Merge branch 'dev'

main
jiangAB 2 months ago
parent
commit
94f118086f
  1. 2
      .env.dev
  2. 2
      .env.local
  3. 2
      .env.prod
  4. 2
      .env.stage
  5. 2
      .env.test
  6. 3
      .eslintrc.js
  7. BIN
      public.zip
  8. BIN
      public/img/未连接.png
  9. BIN
      public/map/.index.vue.swp
  10. 256
      public/map/components/CompanyShorePower.vue
  11. 174
      public/map/components/HistoryRecordDialog.vue
  12. 126
      public/map/components/PieChart.vue
  13. 506
      public/map/components/PortOverview.vue
  14. 426
      public/map/components/ShipHistoryDialog.vue
  15. 1356
      public/map/components/ShipShorePower.vue
  16. 343
      public/map/components/ShorePowerHistoryDialog.vue
  17. 1272
      public/map/components/ShorePowerUsage.vue
  18. 0
      public/map/components/bk/components_index.vue
  19. 2
      public/map/components/bk/map_index.vue
  20. 1398
      public/map/components/cesiumMap.vue
  21. 0
      public/map/components/charts/BarChart.vue
  22. 184
      public/map/components/charts/BarChartMinute.vue
  23. 19
      public/map/components/charts/LineChart.vue
  24. 183
      public/map/components/charts/PieChart.vue
  25. 254
      public/map/components/charts/WaveLineChart.vue
  26. 120
      public/map/components/dictionaryTable.ts
  27. 240
      public/map/components/index.html
  28. 529
      public/map/components/index.vue
  29. 244
      public/map/components/utils.ts
  30. 1117
      public/map/index.vue
  31. 108
      src/api/shorepower/map/index.ts
  32. 4
      src/plugins/elementPlus/index.ts
  33. 479
      src/types/shorepower.d.ts

2
.env.dev

@ -22,7 +22,7 @@ VITE_DROP_CONSOLE=false
VITE_SOURCEMAP=true
# 打包路径
VITE_BASE_PATH='http://server.ayaojies.com.cn:48080'
VITE_BASE_PATH='http://106.118.88.15:48080'
# 输出路径
VITE_OUT_DIR=dist

2
.env.local

@ -4,7 +4,7 @@ NODE_ENV=development
VITE_DEV=true
# 请求路径
VITE_BASE_URL='http://server.ayaojies.com.cn:48080'
VITE_BASE_URL='http://106.118.88.15:48080'
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持 S3 服务
VITE_UPLOAD_TYPE=server

2
.env.prod

@ -4,7 +4,7 @@ NODE_ENV=production
VITE_DEV=false
# 请求路径
VITE_BASE_URL='http://server.ayaojies.com.cn:48080'
VITE_BASE_URL='http://106.118.88.15:48080'
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务
VITE_UPLOAD_TYPE=server

2
.env.stage

@ -4,7 +4,7 @@ NODE_ENV=production
VITE_DEV=false
# 请求路径
VITE_BASE_URL='http://server.ayaojies.com.cn:48080'
VITE_BASE_URL='http://106.118.88.15:48080'
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务
VITE_UPLOAD_TYPE=server

2
.env.test

@ -4,7 +4,7 @@ NODE_ENV=production
VITE_DEV=false
# 请求路径
VITE_BASE_URL='http://server.ayaojies.com.cn:48080'
VITE_BASE_URL='http://106.118.88.15:48080'
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务
VITE_UPLOAD_TYPE=server

3
.eslintrc.js

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

BIN
public.zip

Binary file not shown.

BIN
public/img/未连接.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
public/map/.index.vue.swp

Binary file not shown.

256
public/map/components/CompanyShorePower.vue

@ -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>

174
public/map/components/HistoryRecordDialog.vue

@ -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>

126
public/map/components/PieChart.vue

@ -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>

506
public/map/components/PortOverview.vue

@ -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>

426
public/map/components/ShipHistoryDialog.vue

@ -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>

1356
public/map/components/ShipShorePower.vue

File diff suppressed because it is too large

343
public/map/components/ShorePowerHistoryDialog.vue

@ -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>

1272
public/map/components/ShorePowerUsage.vue

File diff suppressed because it is too large

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

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

@ -366,7 +366,7 @@ const buildDlData = () => {
}
const updateDataDate = (number: number) => {
if (number == 5) {
window.open("http://server.ayaojies.com.cn:801/login", '_blank');
window.open("http://106.118.88.15:801/login", '_blank');
} else {
dataDate.value = number
usedDl.value = 835229.9 * number

1398
public/map/components/cesiumMap.vue

File diff suppressed because it is too large

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

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

@ -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>

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

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

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

@ -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>

254
public/map/components/charts/WaveLineChart.vue

@ -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>

120
public/map/components/dictionaryTable.ts

@ -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 },
]

240
public/map/components/index.html

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

529
public/map/components/index.vue

@ -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 };
// dataJSON
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>

244
public/map/components/utils.ts

@ -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];
}

1117
public/map/index.vue

File diff suppressed because it is too large

108
src/api/shorepower/map/index.ts

@ -1,4 +1,11 @@
import request from '@/config/axios'
import {
apiResponse,
PageResultShipRespVo,
shipHistoryPageRequest,
ShipRespVo,
ShorePowerBerth
} from '@/types/shorepower'
/** 地图信息 */
export interface Map {
@ -70,8 +77,107 @@ export const MapApi = {
// 查询用电接口设备状态
getDeviceStatusByIds: async (ids: any) => {
return await request.get({
url: `/energy/device/getDeviceStatusByIds?ids=${ids.join(',')}`,
url: `/energy/device/getDeviceStatusByIds?ids=${ids.join(',')}`
// params
})
},
// 查询全部设备实时数据
getRealtimeAllData: async (params: any) => {
return await request.get({
url: `/energy/measure-data-realtime/getAll`,
params
})
},
// 查询多个设备实时数据
getRealtimeDataByIdList: async (params: any) => {
return await request.get({
url: `/energy/measure-data-realtime/getByIdList`,
params
})
},
// 查询多个设备年数据
getYearDataByIdList: async (params: any) => {
return await request.get({
url: `/energy/select/year/getByIdList`,
params
})
},
// 查询多个设备周数据
getWeekDataByIdList: async (params: any) => {
return await request.get({
url: `/energy/select/week/getByIdList`,
params
})
},
// 查询多个设备月数据
getMonthDataByIdList: async (params: any) => {
return await request.get({
url: `/energy/select/month/getByIdList`,
params
})
},
// 查询多个设备日数据
getDayDataByIdList: async (params: any) => {
return await request.get({
url: `/energy/select/day/getByIdList`,
params
})
},
// 查询所有船舶和岸电设备ID和名称列表
getBerthIdAndNameList: async () => {
return await request.get({
url: `/shorepower/berth/expand/selectIdAndNameList`
})
},
getShorepowerIdAndNameList: async () => {
return await request.get({
url: `/shorepower/berth/expand/selectIdAndNameList`
})
},
// 查询所有码头和名称列表
getDockIdAndNameList: async () => {
return await request.get({
url: `/shorepower/dock/expand/selectIdAndNameList`
})
},
// 查询所有起运港和到达港和名称列表
getHarborDistrictIdAndNameList: async () => {
return await request.get({
url: `/shorepower/harbor-district/expand/selectIdAndNameList`
})
},
getByStartAndEndTimeAndTimeType: async (params: any) => {
return await request.get({
url: `/energy/select/getByStartAndEndTimeAndTimeType`,
params
})
},
// 查询所有码头和名称列表
getShorepowerIdAndNameListByHarborDistrictId: async (
harborDistrictId: number
): Promise<ShorePowerBerth[]> => {
return await request.get({
url: `/shorepower/shore-power/expand/selectAllByHarborDistrict?harborDistrictId=${harborDistrictId}`
})
},
// 查询船舶历史数据分页
getShipHistoryPage: async (
params: shipHistoryPageRequest
): Promise<apiResponse<PageResultShipRespVo<ShipRespVo>>> => {
return await request.post({
url: `/shorepower/shore-power-and-ship/expand/getHistoryPage`,
params
})
}
}

4
src/plugins/elementPlus/index.ts

@ -1,10 +1,10 @@
import type { App } from 'vue'
// 需要全局引入一些组件,如ElScrollbar,不然一些下拉项样式有问题
import { ElLoading, ElScrollbar, ElButton } from 'element-plus'
import { ElLoading, ElScrollbar, ElButton, ElDialog, ElForm, ElFormItem, ElInput, ElDatePicker, ElTable, ElTableColumn, ElPagination, ElCol, ElRow } from 'element-plus'
const plugins = [ElLoading]
const components = [ElScrollbar, ElButton]
const components = [ElScrollbar, ElButton, ElDialog, ElForm, ElFormItem, ElInput, ElDatePicker, ElTable, ElTableColumn, ElPagination, ElCol, ElRow]
export const setupElementPlus = (app: App<Element>) => {
plugins.forEach((plugin) => {

479
src/types/shorepower.d.ts

@ -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…
Cancel
Save