Compare commits

...

17 Commits

  1. 2
      .env.dev
  2. 2
      .env.local
  3. 2
      .env.prod
  4. 2
      .env.stage
  5. 2
      .env.test
  6. 625
      public/map/components/CompanyShorePower.vue
  7. 741
      public/map/components/ShipShorePower.vue
  8. 547
      public/map/components/ShorePowerUsage.vue
  9. 468
      public/map/components/ShorePowerUsageRate.vue
  10. 1197
      public/map/components/ShorePowerUsageSingleData.vue
  11. 1341
      public/map/components/ShowData.vue
  12. 2
      public/map/components/bk/map_index.vue
  13. 890
      public/map/components/cesiumMap.vue
  14. 196
      public/map/components/charts/ComparisonBarChart.vue
  15. 52
      public/map/components/dictionaryTable.ts
  16. 1067
      public/map/index.vue
  17. 8
      src/api/shorepower/map/index.ts
  18. 96
      src/types/shorepower.d.ts

2
.env.dev

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

2
.env.local

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

625
public/map/components/CompanyShorePower.vue

@ -1,6 +1,123 @@
<template>
<div class="company-shore-power">
<div class="left" style="width: 1000px;">
<div class="left" style="width: 800px;" :style="{ width: selectedShorePowerItem ? '800px' : '420px' }">
<div style="display: flex; gap: 12px; height: 100%;">
<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(getOperationTypeLabel(shorePowerStatus(shorepower), SHORE_POWER_FIRST_STATUS))">
{{ getOperationTypeLabel(shorePowerStatus(shorepower), SHORE_POWER_FIRST_STATUS) }}
</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 v-if="selectedShorePowerItem" 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>
<el-button class="close-btn" type="text" @click="closeShorePowerDetail">×</el-button>
<!-- <div style="ma">
<el-button type="primary" @click="handleOpenHistory">查看历史记录</el-button>
</div> -->
</div>
<div class="card-content">
<div class="ship-detail">
<div class="detail-item">
<span class="label">名称:</span>
<span class="value">{{ selectedShorePowerItem?.name }}</span>
</div>
<div class="detail-item">
<span class="label">位置:</span>
<span class="value">{{ selectedShorePowerItem?.position }}</span>
</div>
<div class="detail-item">
<span class="label">电压:</span>
<span class="value">{{ selectedShorePowerItem?.shorePowerEquipmentInfo?.voltage }}</span>
</div>
<div class="detail-item">
<span class="label">频率:</span>
<span class="value">{{ selectedShorePowerItem?.shorePowerEquipmentInfo?.frequency }} </span>
</div>
<div class="detail-item">
<span class="label">容量:</span>
<span class="value">{{ selectedShorePowerItem?.shorePowerEquipmentInfo?.capacity }}</span>
</div>
<div class="detail-item">
<span class="label">当前电压:</span>
<span class="value">{{ getValueById(realtimeDeviceData, selectedShorePowerItem.voltageDeviceId,
'measureValue') }}</span>
</div>
<div class="detail-item">
<span class="label">当前电流:</span>
<span class="value">{{ getValueById(realtimeDeviceData, selectedShorePowerItem.currentDeviceId,
'measureValue') }}</span>
</div>
<div class="detail-item">
<span class="label">当前频率:</span>
<span class="value">{{ getValueById(realtimeDeviceData, selectedShorePowerItem.frequencyDeviceId,
'measureValue') }}</span>
</div>
<div class="detail-item">
<span class="label">当前总用量:</span>
<span class="value">{{ getValueById(realtimeDeviceData, selectedShorePowerItem.totalPowerDeviceId,
'measureValue') }}</span>
</div>
<div class="detail-item">
<span class="label">当前状态:</span>
<span class="value">{{ getOperationTypeLabel(shorePowerStatus(selectedShorePowerItem),
SHORE_POWER_FIRST_STATUS,
) }}({{ getOperationTypeLabel(shorePowerStatus(selectedShorePowerItem),
SHORE_POWER_SECOND_STATUS_MAP,
) }})</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="right" 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;">
@ -9,6 +126,9 @@
<img src="@/assets/svgs/data.svg" class="title-icon" />
<span class="title-text">码头与泊位信息</span>
</div>
<!-- 返回上个页面 -->
<el-button type="primary" @click.stop="handleGoBack()">返回</el-button>
<!-- <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" />
@ -18,41 +138,60 @@
<div class="card-content">
<div v-for="company in companyComparisonData" :key="company.name" class="company-section">
<div class="data-update-time">起始时间: {{ getStartTimeByRange(getCompanyTimeRange(company.id)) }} 更新时间: {{
realtimeDeviceDataTime }}</div>
<div class="company-header">
<div class="company-name">{{ company.name }}</div>
<div class="company-total">总量: {{ calculateTotal(company.children).toFixed(2) }}</div>
<div class="company-info">
<div class="company-name">{{ company.name }}</div>
<div class="company-berth-count">泊位数: {{ company.shorePowerCount || 0 }}</div>
</div>
<div class="time-range-container">
<button v-for="option in timeRangeOptions" :key="option.value"
:class="['time-range-btn', { active: getCompanyTimeRange(company.id) === option.value }]"
@click.stop="() => handleTimeRangeChange(company.id, option.value)">
{{ option.label }}
</button>
<div class="company-total">总量: {{ handGetDeviceData(company.shorePowerIds,
getCompanyTimeRange(company.id)) }}</div>
</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="shorepower-container">
<div class="diste-box" v-for="(disteItem, index) in (company.distributionList || [])" :key="index">
<div class="diste-name">{{ disteItem.name }}</div>
<div class="shorepower-box">
<div class="shorepower-item" v-for="(shorePower, shorePowerIndex) in (disteItem.shorePowerList || [])"
:key="shorePowerIndex">
<div class="shorepower-value" @click="handleClickhorePowerValue(shorePower)">{{
handGetDeviceData(shorePower.totalPowerDeviceId, getCompanyTimeRange(company.id)) || 0 }}</div>
<div class="overview-label">{{ berth.name }}</div>
<div class="shorepower-label" @click="handleClickShorePowerName(shorePower)">{{ shorePower.name }}
</div>
</div>
</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" />
<ship-history-dialog v-model="historyVisible.visible" :ship-param="historyVisible.searchParams"
:realtime-device-data="realtimeDeviceData" />
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { ref, computed } from 'vue'
import { MapApi } from "@/api/shorepower/map";
import ShipHistoryDialog from './ShipHistoryDialog.vue';
import { RealtimeDeviceData } from '@/types/shorepower';
import { CompanyShorePowerData, RealtimeDeviceData, ShipRespVo, ShorePowerBerth } from '@/types/shorepower';
import { getValueById } from './utils';
import { getOperationTypeLabel, SHORE_POWER_FIRST_STATUS, SHORE_POWER_SECOND_STATUS_MAP } from './dictionaryTable';
//
interface ChartDataItem {
name: string;
value?: number;
measureValue?: number;
children?: ChartDataItem[];
}
const historyVisible = ref({
visible: false,
@ -63,23 +202,171 @@ const historyVisible = ref({
}
})
//
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
} */
interface Props {
realtimeDeviceData: RealtimeDeviceData[];
realtimeDeviceDataTime: string;
shorePowerList: ShorePowerBerth[];
handleGoBack: () => void;
yearData: any[];
monthData: any[];
dayData: any[];
companyComparisonData: CompanyShorePowerData[];
}
const props = defineProps<Props>()
const selectedShorePowerItem = ref<ShorePowerBerth>()
const shorepowerSelectedItem = ref<ShorePowerBerth & { position: string; } | null>(null)
const storeSearchKeyword = ref('')
//
interface TimeRangeOption {
value: 'day' | 'month' | 'year' | 'realtime';
label: string;
}
const timeRangeOptions: TimeRangeOption[] = [
{ value: 'day', label: '当日' },
{ value: 'month', label: '当月' },
{ value: 'year', label: '当年' },
{ value: 'realtime', label: '汇总' },
]
const closeShorePowerDetail = () => {
selectedShorePowerItem.value = undefined
}
//
const handlStoreSearch = () => {
//
}
const shorePowerStatus = (item: ShorePowerBerth) => {
if (!item.totalPowerDeviceId) {
return ''
}
const status = getDeviceStatus(item.totalPowerDeviceId)
if (status === null) {
return ''
}
return status
}
/* 用户实时设备状态 */
const getDeviceStatus = (deviceId: number) => {
const data = props.realtimeDeviceData.find(item => item.deviceId === deviceId)
if (!data) {
return null
}
return data.deviceStatus
}
const getStatusClass = (status: string | undefined) => {
switch (status) {
case '正常':
return 'status-default'
case '在用':
return 'status-normal'
case '故障':
return 'status-fault'
default:
return 'status-normal'
}
}
//
const filteredShorePowerList = computed(() => {
if (!storeSearchKeyword.value) {
return props.shorePowerList
}
return props.shorePowerList.filter(shorepower =>
shorepower.name.toLowerCase().includes(storeSearchKeyword.value.toLowerCase())
)
})
//
const emit = defineEmits<{
(e: 'switch-ship', ship: ShipRespVo): void;
(e: 'item-click', item: any): void;
// handleSelectItem(ship)
}>()
const handleSelectShorePower = async (shorepower: ShorePowerBerth & { position: string }) => {
// selectedItem.value = shorepower
shorepowerSelectedItem.value = shorepower
selectedShorePowerItem.value = shorepower
emit('item-click', {
type: 'shorepower_box',
item: shorepower
})
// const data = await MapApi.getRealtimeDataByIdList({ ids: shorepower.totalPowerDeviceId })
// console.log('voltageDeviceId', data)
}
//
const companyTimeRanges = ref<Record<number, 'realtime' | 'day' | 'month' | 'year'>>({})
// 'day'
const getCompanyTimeRange = (companyId: number) => {
return companyTimeRanges.value[companyId] || 'day'
}
//
const getStartTimeByRange = (range: 'realtime' | 'day' | 'month' | 'year') => {
const now = new Date();
switch (range) {
case 'day':
// 00:00:00
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
return formatDate(today);
case 'month':
// 00:00:00
const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
return formatDate(firstDayOfMonth);
case 'year':
// 00:00:00
const firstDayOfYear = new Date(now.getFullYear(), 0, 1);
return formatDate(firstDayOfYear);
case 'realtime':
//
return '2020-01-01 00:00:00';
default:
return formatDate(new Date(now.getFullYear(), now.getMonth(), now.getDate()));
}
}
// YYYY-MM-DD HH:mm:ss
const formatDate = (date: Date) => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day} 00:00:00`;
}
//
const handleTimeRangeChange = (companyId: number, range: 'realtime' | 'day' | 'month' | 'year') => {
companyTimeRanges.value[companyId] = range
//
}
/* 点击泊位box */
const handleClickShorePowerName = (data: CompanyShorePowerData) => {
const shorePower = props.shorePowerList.find(item => item.id === data.id)
selectedShorePowerItem.value = shorePower
console.log(shorePower)
}
const handleClickhorePowerValue = (data: CompanyShorePowerData) => {
const shorePower = props.shorePowerList.find(item => item.id === data.id)
if (!shorePower) {
return
}
showShorePowerHistory(shorePower)
/* selectedShorePowerItem.value = shorePower
console.log(shorePower) */
}
const showShorePowerHistory = (berth: ShorePowerBerth) => {
historyVisible.value = {
visible: true,
searchParams: {
@ -89,47 +376,103 @@ const showShorePowerHistory = (berth: ChartDataItem) => {
}
}
}
// const companyComparisonData = ref<companyData[]>([])
interface Props {
companyComparisonData?: ChartDataItem[];
realtimeDeviceData: RealtimeDeviceData[];
//
const deviceDataMap = computed(() => {
const map = new Map<number, RealtimeDeviceData>();
props.realtimeDeviceData.forEach(item => {
map.set(item.deviceId, item);
});
return map;
});
const handGetDeviceData = (deviceId: number | number[], range: 'realtime' | 'day' | 'month' | 'year') => {
let totalValue = 0;
let rangData: RealtimeDeviceData[] = []
if (range === 'realtime') {
rangData = props.realtimeDeviceData
} else if (range === 'day') {
rangData = props.dayData
} else if (range === 'month') {
rangData = props.monthData
} else if (range === 'year') {
rangData = props.yearData
}
if (Array.isArray(deviceId)) {
// If deviceId is an array, sum all the measure values and subtract start values
deviceId.forEach(id => {
const deviceData = deviceDataMap.value.get(id);
const startData = rangData.find(item => item.deviceId === id)
if (deviceData?.measureValue) {
totalValue += deviceData.measureValue;
}
// For non-realtime ranges, subtract the start value
if (range !== 'realtime' && startData?.measureValue) {
totalValue -= startData.measureValue;
}
});
} else {
// If deviceId is a single number, get its measure value
const deviceData = deviceDataMap.value.get(deviceId);
const startData = rangData.find(item => item.deviceId === deviceId)
if (deviceData?.measureValue) {
totalValue = deviceData.measureValue;
}
if (range !== 'realtime' && startData?.measureValue) {
totalValue -= startData.measureValue
}
}
return totalValue.toFixed(2);
}
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()
// const handleGetBuildData = async () => {
// const dockList = await MapApi.getDockIdAndNameList()
// // const berthList = await MapApi.getBerthIdAndNameList()
// const distributionList = groupByShorePowerEquipmentId(props.shorePowerList)
// const buildData = dockList.map(dock => {
// const filterData = distributionList.filter(item => item.dockId === dock.id)
// const shorePowerCount = filterData.reduce((total, item) => {
// return total + item.shorePowerList.length
// }, 0)
// return {
// ...dock,
// shorePowerCount: shorePowerCount, //
// shorePowerIds: filterData.map(item => item.shorePowerList.map(shorePower => shorePower.id)).flat(),
// distributionList: filterData
// }
// })
// companyComparisonData.value = buildData
// /* */
// function groupByShorePowerEquipmentId(data) {
// const map = new Map();
// for (const item of data) {
// const { shorePowerEquipmentInfo, ...rest } = item;
// const id = shorePowerEquipmentInfo.id;
// if (!map.has(id)) {
// map.set(id, {
// ...shorePowerEquipmentInfo,
// shorePowerList: []
// });
// }
// map.get(id).shorePowerList.push(rest);
// }
// return Array.from(map.values());
// }
// }
onMounted(() => {
// handleGetBuildData()
})
</script>
<style scoped>
<style scoped lang="scss">
.company-shore-power {
display: flex;
height: 100%;
@ -158,46 +501,88 @@ onMounted(() => {
}
/* 总览网格样式 */
.overview-grid {
/* .overview-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
gap: 8px;
padding: 4px;
height: 100%;
}
.overview-item {
cursor: pointer;
} */
.shorepower-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 4px;
background-color: rgba(0, 0, 0, 0.2);
border-radius: 6px;
}
gap: 8px;
flex-wrap: wrap;
.overview-label {
text-align: center;
font-size: 24px;
color: #ccc;
margin-bottom: 2px;
}
.diste-box {
flex-shrink: 0;
display: inline-block;
margin-bottom: 8px;
.overview-value {
font-size: 36px;
font-weight: bold;
color: #1296db;
text-shadow: 0 0 5px rgba(0, 0, 0, 0.5);
margin-bottom: 2px;
}
.diste-name {
height: 36px;
background-color: rgba(0, 112, 192, 0.579);
border-radius: 6px 6px 0 0;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
font-weight: bold;
color: #fff;
padding: 4px;
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
}
}
.shorepower-box {
display: flex;
gap: 8px;
// flex-wrap: wrap;
}
.shorepower-item {
display: inline-flex;
width: 224px;
// display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 4px;
background-color: rgba(30, 120, 255, 0.15);
border-radius: 0 0 8px 8px;
}
.shorepower-label {
text-align: center;
font-size: 24px;
color: #ccc;
margin-bottom: 2px;
cursor: pointer;
transition: all 0.3s ease;
.overview-text {
font-size: 10px;
color: #999;
text-align: center;
&:hover {
//
transform: translateY(-2px);
}
}
.shorepower-value {
font-size: 36px;
font-weight: bold;
color: #1296db;
text-shadow: 0 0 5px rgba(0, 0, 0, 0.5);
margin-bottom: 2px;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
//
transform: translateY(-2px);
}
}
}
/* 企业区块样式 */
.company-section {
margin-bottom: 16px;
@ -220,12 +605,24 @@ onMounted(() => {
padding: 0 4px;
}
.company-info {
display: flex;
align-items: baseline;
gap: 2px
}
.company-name {
font-size: 24px;
font-weight: bold;
color: #fff;
}
.company-berth-count {
font-size: 18px;
color: #eee;
margin-top: 4px;
}
.company-total {
font-size: 24px;
font-weight: bold;
@ -235,6 +632,13 @@ onMounted(() => {
border-radius: 4px;
}
.data-update-time {
margin-left: 6px;
font-size: 24px;
color: #aaa;
white-space: nowrap;
}
/* 自定义滚动条样式 */
.card-content::-webkit-scrollbar {
width: 6px;
@ -253,4 +657,27 @@ onMounted(() => {
.card-content::-webkit-scrollbar-thumb:hover {
background: rgba(17, 138, 237, 1);
}
.time-range-container {
display: flex;
align-items: center;
gap: 4px;
}
.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);
}
.close-btn {
width: 36px;
height: 36px;
margin-bottom: 8px;
}
</style>

741
public/map/components/ShipShorePower.vue

@ -1,25 +1,36 @@
<template>
<div class="ship-shore-power">
<div class="left" style="width: 70%;">
<div class="left" style="width: 75%;">
<div class="card digital-twin-card--deep-blue" style="flex: 1;">
<div class="card-title">
<div class="vertical-line"></div>
<img src="@/assets/svgs/data.svg" class="title-icon" />
<span class="title-text">概览</span>
<div style="display: flex; justify-content: space-between; align-items: center;">
<div class="card-title">
<div class="vertical-line"></div>
<img src="@/assets/svgs/data.svg" class="title-icon" />
<span class="title-text">概览</span>
<div class="time-range-item">起始时间: {{ startTimeDisplay }}</div>
<div class="time-range-item">更新时间: {{ realtimeDeviceDataTime }}</div>
</div>
<el-button type="primary" @click.stop="handleGoBack()">返回</el-button>
</div>
<div class="card-content">
<div class="overview-grid">
<div class="overview-item">
<div class="overview-value">{{ berthingShips }}</div>
<div class="overview-label">在泊船舶数量</div>
<div class="overview-value">{{ shipTotalData.allPortAreaCount }}</div>
<div class="overview-label">港船舶</div>
</div>
<div class="overview-item">
<div class="overview-value">{{ shorePowerShips }}</div>
<div class="overview-label">使用岸电船舶数量</div>
<div class="overview-value">{{ shipTotalData.berthingShips }}</div>
<div class="overview-label">在泊船舶</div>
</div>
<div class="overview-item">
<div class="overview-value">{{ noShorePowerShips }}</div>
<div class="overview-label">未使用岸电船舶数量</div>
<div class="overview-value">{{ shipTotalData.shorePowerShips }}</div>
<div class="overview-label">使用岸电船舶</div>
</div>
<div class="overview-item">
<div class="overview-value">{{ shipTotalData.noShorePowerShips }}</div>
<div class="overview-label">未使用岸电船舶</div>
</div>
</div>
</div>
@ -46,28 +57,54 @@
</div>
<div class="card-content">
<div class="ship-data-table-container">
<el-table :data="filteredShipData" class="ship-data-table" @row-click="handleShipClick"
<el-table :data="filteredShipData" class="ship-data-table" @row-click="handleClickShipItem"
highlight-current-row>
<el-table-column prop="name" label="船名" />
<el-table-column prop="wharf" label="码头" />
<el-table-column prop="berth" label="泊位" />
<el-table-column prop="shipBasicInfo.name" label="船名" />
<el-table-column prop="applyInfo.arrivalDock" label="码头">
<template #default="scope">
<span>{{ getOperationTypeLabel(scope.row.applyInfo?.arrivalDock, dockIdAndNameList) }}</span>
</template>
</el-table-column>
<el-table-column prop="applyInfo.arrivalBerth" label="泊位">
<template #default="scope">
<span>{{ getOperationTypeLabel(scope.row.applyInfo?.arrivalBerth, berthIdAndNameList) }}</span>
</template>
</el-table-column>
<el-table-column prop="beginTime" label="开始用电时间">
<template #default="scope">
<span>{{ formatDateTime(scope.row.beginTime) }}</span>
<span>{{ formatDateTime(scope.row?.usageRecordInfo?.beginTime) }}</span>
</template>
</el-table-column>
<el-table-column prop="endTime" label="结束用电时间">
<template #default="scope">
<span>{{ formatDateTime(scope.row.endTime) }}</span>
<span>{{ formatDateTime(scope.row?.usageRecordInfo?.endTime) }}</span>
</template>
</el-table-column>
<!-- <el-table-column prop="shipStatus" label="船舶状态">
<template #default="scope">
<span :class="[getStatusClass(scope.row.shipStatus)]">{{ scope.row.shipStatus }}</span>
</template>
</el-table-column> -->
<el-table-column prop="status" label="岸电状态" width="400">
<template #default="scope">
<span :class="['shore-power-status', scope.row.statusClass]">{{ scope.row.status }}</span>
<span v-if="scope.row.applyInfo?.reason === 0"
:class="['shore-power-status', showStatus(scope.row, realtimeDeviceData)?.statusClass]">用时{{
showStatus(scope.row,
realtimeDeviceData)?.useTime }},电量{{
showStatus(scope.row, realtimeDeviceData)?.useValue }}
</span>
<span v-if="scope.row.applyInfo?.reason != 0" class="shore-power-status status-off">{{
getOperationTypeLabel(scope.row.applyInfo?.reason,
UNUSED_SHORE_POWER_REASON) }}</span>
</template>
</el-table-column>
<el-table-column prop="applyInfo.departureHarborDistrict" label="起运港" />
<el-table-column prop="arrivalHarborDistrict" label="到达港">
<template #default="scope">
<span>{{ getOperationTypeLabel(scope.row.applyInfo?.arrivalHarborDistrict,
harborDistrictIdAndNameList) }}</span>
</template>
</el-table-column>
<el-table-column prop="departureHarborDistrict" label="起运港" />
<el-table-column prop="arrivalHarborDistrict" label="到达港" />
<el-table-column label="操作" width="100">
<template #default="scope">
<el-button type="primary" size="small" @click.stop="showDetail(scope.row)">更多</el-button>
@ -78,8 +115,98 @@
</div>
</div>
</div>
<div class="right" v-if="selectedShip">
<div class="card digital-twin-card--deep-blue">
<div class="card-title">
<div class="vertical-line"></div>
<img src="@/assets/svgs/data.svg" class="title-icon" />
<span class="title-text">船舶详情</span>
</div>
<div class="card-content">
<div class="ship-detail">
<div class="detail-item">
<span class="label">名称:</span>
<span class="value">{{ selectedShip.shipBasicInfo.name + '-' + selectedShip.shipBasicInfo.nameEn
}}</span>
</div>
<div class="detail-item">
<span class="label">长度:</span>
<span class="value">{{ selectedShip.shipBasicInfo.length }} </span>
</div>
<div class="detail-item">
<span class="label">宽度:</span>
<span class="value">{{ selectedShip.shipBasicInfo.width }} </span>
</div>
<div class="detail-item">
<span class="label">吨位:</span>
<span class="value">{{ selectedShip.shipBasicInfo.tonnage }} </span>
</div>
<div class="detail-item">
<span class="label">满载吃水深度:</span>
<span class="value">{{ selectedShip.shipBasicInfo.fullLoadDraft }} </span>
</div>
<div class="detail-item">
<span class="label">电压:</span>
<span class="value">{{ getValueById(realtimeDeviceData, selectedShip.shorePower.voltageDeviceId,
'measureValue') }}</span>
</div>
<div class="detail-item">
<span class="label">电流:</span>
<span class="value">{{ getValueById(realtimeDeviceData, selectedShip.shorePower.currentDeviceId,
'measureValue') }}</span>
</div>
<div class="detail-item">
<span class="label">频率:</span>
<span class="value">{{ getValueById(realtimeDeviceData, selectedShip.shorePower.frequencyDeviceId,
'measureValue') }}</span>
</div>
<div class="detail-item">
<span class="label">靠泊状态:</span>
<span class="value">{{ getOperationTypeLabel(selectedShip.shorePowerAndShip.status,
SHORE_POWER_STATUS,
) }}</span>
</div>
<div class="detail-item">
<span class="label">靠泊类型:</span>
<span class="value">{{ getOperationTypeLabel(selectedShip.shorePowerAndShip.type, BERTH_TYPE)
}}</span>
</div>
<div class="detail-item">
<span class="label">靠泊时间:</span>
<span class="value">{{ formatTimestamp(selectedShip?.usageRecordInfo?.actualBerthTime) }}</span>
</div>
<div class="detail-item">
<span class="label">当前状态:</span>
<span class="value">{{ selectedShip.shipStatus }}</span>
</div>
<div v-if="selectedShip.applyInfo.reason === 0" class="detail-item">
<span class="label">岸电使用时长:</span>
<span class="value">{{ showStatus(selectedShip, realtimeDeviceData)?.useTime }}</span>
</div>
<div v-if="selectedShip.applyInfo.reason === 0" class="detail-item">
<span class="label">岸电使用用量:</span>
<span class="value">{{ showStatus(selectedShip, realtimeDeviceData)?.useValue }}</span>
</div>
<div v-if="selectedShip.applyInfo.reason != 0" class="detail-item">
<span class="label">未使用岸电原因:</span>
<span class="value">{{ getOperationTypeLabel(selectedShip?.applyInfo?.reason,
UNUSED_SHORE_POWER_REASON) }}</span>
</div>
<div class="detail-item">
<span class="label">岸电联系人:</span>
<span class="value">{{ selectedShip.shipBasicInfo.shorePowerContact }}</span>
</div>
<div class="detail-item">
<span class="label">联系方式:</span>
<span class="value">{{ selectedShip.shipBasicInfo.shorePowerContactPhone }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- 船舶详情对话框 -->
<el-dialog v-model="detailDialogVisible" title="更多信息" width="1000px" class="ship-detail-dialog">
<el-dialog v-if="detailDialogVisible" v-model="detailDialogVisible" title="备案信息" width="1000px"
class="ship-detail-dialog">
<el-tabs v-if="selectedShip" v-model="activeTab">
<el-tab-pane label="用电申请信息" name="applyInfo">
<el-form :model="selectedShip.applyInfo" label-width="200px">
@ -108,8 +235,9 @@
</el-col>
<el-col :span="12">
<el-form-item label="到达港:">
<span>{{ findNameById(selectedShip.applyInfo?.arrivalHarborDistrict, harborDistrictIdAndNameList)
}}</span>
<span>{{ getOperationTypeLabel(selectedShip.applyInfo?.arrivalHarborDistrict,
harborDistrictIdAndNameList)
}}</span>
</el-form-item>
</el-col>
</el-row>
@ -117,12 +245,12 @@
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="到达码头:">
<span>{{ findNameById(selectedShip.applyInfo?.arrivalDock, dockIdAndNameList) }}</span>
<span>{{ getOperationTypeLabel(selectedShip.applyInfo?.arrivalDock, dockIdAndNameList) }}</span>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="到达泊位:">
<span>{{ findNameById(selectedShip.applyInfo?.arrivalBerth, berthIdAndNameList) }}</span>
<span>{{ getOperationTypeLabel(selectedShip.applyInfo?.arrivalBerth, berthIdAndNameList) }}</span>
</el-form-item>
</el-col>
</el-row>
@ -164,7 +292,7 @@
<el-col :span="12">
<el-form-item label="货物类型(装货):">
<span>{{ getOperationTypeLabel(selectedShip.applyInfo?.loadingCargoCategory, CARGO_CATEGORY) || '-'
}}</span>
}}</span>
</el-form-item>
</el-col>
<el-col :span="12">
@ -186,7 +314,7 @@
<el-col :span="12">
<el-form-item label="货物类型(卸货):">
<span>{{ getOperationTypeLabel(selectedShip.applyInfo?.unloadingCargoCategory, CARGO_CATEGORY) || '-'
}}</span>
}}</span>
</el-form-item>
</el-col>
<el-col :span="12">
@ -316,8 +444,9 @@
<span>{{ selectedShip.usageRecordInfo?.beginPowerSupplyOperator || '-' }}</span>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="用电开始时用电方操作人:">
<el-form-item label="用电开始时方操作人:">
<span>{{ selectedShip.usageRecordInfo?.beginPowerUsageOperator || '-' }}</span>
</el-form-item>
</el-col>
@ -330,7 +459,7 @@
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="用电结束时用电方操作人:">
<el-form-item label="用电结束时方操作人:">
<span>{{ selectedShip.usageRecordInfo?.overPowerUsageOperator || '-' }}</span>
</el-form-item>
</el-col>
@ -481,295 +610,92 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { MapApi } from "@/api/shorepower/map";
import dayjs from 'dayjs';
import { ShipRespVo } from '@/types/shorepower';
import { APPLY_STATUS, CARGO_CATEGORY, getOperationTypeLabel, OPERATION_TYPE, TRADE_TYPE, UNUSED_SHORE_POWER_REASON, WORK_STATUS } from './dictionaryTable';
//
interface ShipDataItem {
name: string;
wharf: string;
berth: string;
status: string;
statusClass: string;
departureHarborDistrict: string;
arrivalHarborDistrict: string;
beginTime?: Date;
endTime?: Date;
shipBasicInfo?: {
/**
* 船舶呼号
*/
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;
}
applyInfo?: {
/**
* 船方代理单位
*/
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;
}
usageRecordInfo?: {
/**
* 实际靠泊时间
*/
actualBerthTime?: Date;
/**
* 实际离泊时间
*/
actualDepartureTime?: Date;
/**
* 用电申请编号
*/
applyId: number;
/**
* 用电开始时供电方操作人
*/
beginPowerSupplyOperator?: string;
/**
* 用电开始时用电方操作人
*/
beginPowerUsageOperator?: string;
/**
* 用电开始时间
*/
beginTime?: Date;
/**
* 创建时间
*/
createTime: Date;
/**
* 用电结束时间
*/
endTime?: Date;
/**
* 编号
*/
id: number;
/**
* 用电结束时供电方操作人
*/
overPowerSupplyOperator?: string;
/**
* 用电结束时用电方操作人
*/
overPowerUsageOperator?: string;
/**
* 用电结束时间人工读数
*/
powerEndManualReading?: number;
/**
* 用电结束时间系统读数
*/
powerEndSystemReading?: number;
/**
* 用电开始时间人工读数
*/
powerStartManualReading?: number;
/**
* 用电开始时间系统读数
*/
powerStartSystemReading?: number;
/**
* 船舶编号
*/
shipId: number;
/**
* 作业单状态
*/
status: number;
[property: string]: any;
}
shorePower?: {
totalPowerDeviceId: string;
};
}
import { RealtimeDeviceData, ShipRespVo } from '@/types/shorepower';
import { APPLY_STATUS, BERTH_DISTRICT, BERTH_TYPE, CARGO_CATEGORY, DOCK_DISTRICT, getOperationTypeLabel, HARBOR_DISTRICT, OPERATION_TYPE, SHORE_POWER_STATUS, TRADE_TYPE, UNUSED_SHORE_POWER_REASON, WORK_STATUS } from './dictionaryTable';
import { formatTimestamp, getValueById, showStatus } from './utils';
interface Props {
shipData: ShipRespVo[];
realtimeDeviceData: RealtimeDeviceData[];
realtimeDeviceDataTime: string;
shipTotalData: {
allPortAreaCount: number;
berthingShips: number;
shorePowerShips: number;
noShorePowerShips: number;
};
handleGoBack: () => void;
}
const props = defineProps<Props>()
//
const emit = defineEmits<{
(e: 'switch-ship', ship: ShipDataItem): void;
(e: 'item-click', ship: ShipDataItem): void;
(e: 'switch-ship', ship: ShipRespVo): void;
(e: 'item-click', ship: ShipRespVo): void;
}>()
const localShipData = ref<ShipDataItem[]>([])
const localShipData = ref<(ShipRespVo)[]>([])
const dateRange = ref<[string, string] | []>([])
const statusFilter = ref<string>('all') // 'all', 'using', 'not-using'
//
const detailDialogVisible = ref(false)
//
const selectedShip = ref<ShipDataItem | null>(null)
const selectedShip = ref<ShipRespVo | null>(null)
// tab
const activeTab = ref('applyInfo')
// API
const berthIdAndNameList = ref<{ id: string, name: string }[]>([])
const dockIdAndNameList = ref<{ id: string, name: string }[]>([])
const harborDistrictIdAndNameList = ref<{ id: string, name: string }[]>([])
const berthIdAndNameList = ref<{ value: number, label: string }[]>([])
const dockIdAndNameList = ref<{ value: number, label: string }[]>([])
const harborDistrictIdAndNameList = ref<{ value: number, label: string }[]>([])
// localShipData
const berthingShips = computed(() => localShipData.value.length)
const shorePowerShips = computed(() => localShipData.value.filter(ship => ship.statusClass === 'status-using').length)
const noShorePowerShips = computed(() => berthingShips.value - shorePowerShips.value)
const shorePowerShips = computed(() => localShipData.value.filter(ship => ship.applyInfo.reason === 0).length)
const noShorePowerShips = computed(() => localShipData.value.filter(ship => ship.applyInfo.reason !== 0).length)
// filteredShipData
const filteredBerthingShips = computed(() => filteredShipData.value.length)
const filteredShorePowerShips = computed(() => filteredShipData.value.filter(ship => ship.statusClass === 'status-using').length)
const filteredNoShorePowerShips = computed(() => filteredBerthingShips.value - filteredShorePowerShips.value)
//
const startTimeDisplay = computed(() => {
const startOfDay = dayjs().startOf('day');
return startOfDay.format('YYYY-MM-DD HH:mm:ss');
})
// CSS
const getStatusClass = (status: string | undefined) => {
switch (status) {
case '正常':
return 'status-normal-1'
case '在线':
return 'status-normal-1'
case '空闲':
return 'status-idle-1'
case '故障':
return 'status-fault-1'
case '超容':
return 'status-maintenance-1'
case '异常':
return 'status-abnormal-1'
case '维修中':
return 'status-maintenance-1'
case '岸电使用中':
return 'status-shorepower-1'
// case '':
// return 'status-fault'
default:
return 'status-default-1'
}
}
//
const filteredShipData = computed(() => {
//
let statusFilteredData = localShipData.value;
if (statusFilter.value === 'using') {
statusFilteredData = localShipData.value.filter(ship => ship.statusClass === 'status-using');
statusFilteredData = localShipData.value.filter(ship => ship.applyInfo.reason === 0);
} else if (statusFilter.value === 'not-using') {
statusFilteredData = localShipData.value.filter(ship => ship.statusClass !== 'status-using');
statusFilteredData = localShipData.value.filter(ship => ship.applyInfo.reason !== 0);
}
//
@ -832,42 +758,9 @@ const resetFilter = () => {
};
//
const handleShipClick = (row: ShipDataItem, column: any, event: Event) => {
// emit('switch-ship', row)
emit('item-click', row)
}
// ID
const findNameById = (id: string | number | undefined, list: { id: string, name: string }[]) => {
// undefinednull
if (id === undefined || id === null) {
return '-';
}
//
const idStr = id.toString();
const item = list.find(item => item.id == idStr)
return item ? item.name : idStr // ID
}
// 使
const getReasonText = (reason: number | undefined) => {
if (reason === undefined) return '-';
//
const reasonMap: Record<number, string> = {
1: '岸电用电接口不匹配',
2: '岸电设施电压/频率不匹配',
3: '电缆长度不匹配',
4: '气象因素禁止作业',
5: '船电设施损坏',
6: '岸电设施维护中',
7: '无受电设备',
8: '拒绝使用岸电',
9: '其他'
}
return reasonMap[reason] || reason.toString()
const handleClickShipItem = (row: ShipRespVo) => {
console.log(row)
selectedShip.value = row
}
//
@ -875,155 +768,18 @@ const formatDateTime = (timestamp: string | number | Date | undefined) => {
if (!timestamp) return '-';
return dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss');
}
const showStatus = async (ship: ShipDataItem) => {
const { usageRecordInfo, applyInfo } = ship;
if (applyInfo && applyInfo.reason !== undefined && 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 realtimeData = await MapApi.getRealtimeDataByIdList({ ids: deviceId });
if (realtimeData && realtimeData.length > 0) {
const measureValue = realtimeData[0].measureValue;
// 使
const startReading = usageRecordInfo.powerStartManualReading ?? usageRecordInfo.powerStartSystemReading ?? 0;
useValue = measureValue - startReading;
}
} catch (error) {
console.error('获取实时数据失败:', error);
useValue = 0;
}
}
}
return {
statusClass: 'status-using',
status: `使用岸电${hours}小时${minutes}分钟${seconds}秒,${useValue.toFixed(2)}kWH`
}
} else if (applyInfo && applyInfo.reason !== undefined && applyInfo.reason != 0) {
// -
const statusMap: Record<string, { text: string; colorType: string }> = {
'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()
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'
}
}
}
const buildData = async (ship: ShipDataItem) => {
// const { name, wharf, berth, status, statusClass, usageRecordInfo } = ship;
const statusResult = await showStatus(ship) || {};
const { statusClass, status } = statusResult;
return {
applyInfo: ship.applyInfo,
usageRecordInfo: ship.usageRecordInfo,
shipBasicInfo: ship.shipBasicInfo,
name: ship?.shipBasicInfo?.name,
wharf: findNameById(ship.applyInfo?.arrivalDock.toString(), dockIdAndNameList.value),
berth: findNameById(ship.applyInfo?.arrivalBerth.toString(), berthIdAndNameList.value),
beginTime: ship.usageRecordInfo?.beginTime,
endTime: ship.usageRecordInfo?.endTime,
departureHarborDistrict: ship.applyInfo?.departureHarborDistrict,
arrivalHarborDistrict: findNameById(ship.applyInfo?.arrivalHarborDistrict.toString(), harborDistrictIdAndNameList.value),
status: status,
statusClass: statusClass,
}
}
//
const showDetail = (row: ShipDataItem) => {
console.log('first')
const showDetail = (row: ShipRespVo) => {
selectedShip.value = row
detailDialogVisible.value = true
}
onMounted(async () => {
// API
berthIdAndNameList.value = await MapApi.getBerthIdAndNameList()
dockIdAndNameList.value = await MapApi.getDockIdAndNameList()
harborDistrictIdAndNameList.value = await MapApi.getHarborDistrictIdAndNameList()
console.log('props.shipData', props.shipData)
// 使Promise.all
localShipData.value = await Promise.all(props.shipData.map(ship => buildData(ship)));
berthIdAndNameList.value = await BERTH_DISTRICT()
dockIdAndNameList.value = await DOCK_DISTRICT()
harborDistrictIdAndNameList.value = await HARBOR_DISTRICT()
localShipData.value = props.shipData
})
</script>
@ -1353,4 +1109,57 @@ onMounted(async () => {
background-color: rgba(255, 255, 255, 0.2);
border: 1px solid rgba(255, 255, 255, 0.5);
}
.time-range-item {
margin-left: 24px;
font-size: 24px;
font-weight: 600;
font-weight: bold;
color: rgba(255, 255, 255, 0.7);
}
.status-normal-1 {
color: #00ff00;
font-weight: bold;
}
/* 空闲状态 */
.status-idle-1 {
color: #2196F3;
/* 蓝色 */
font-weight: bold;
}
/* 故障状态 */
.status-fault-1 {
color: #F44336;
/* 红色 */
font-weight: bold;
}
.status-abnormal-1 {
color: #F44336;
font-weight: bold;
}
.status-maintenance-1 {
color: #FF9800;
font-weight: bold;
}
.status-shorepower-1 {
color: #2196F3;
font-weight: bold;
}
/* .status-fault {
background-color: #9C27B0;
box-shadow: 0 0 5px rgba(156, 39, 176, 0.5);
} */
.status-default-1 {
color: #b4babd;
font-weight: bold;
}
</style>

547
public/map/components/ShorePowerUsage.vue

@ -1,143 +1,7 @@
<template>
<div class="shore-power-usage">
<template v-if="pageType === 'realtime'">
<!-- Average Mode -->
<template v-if="displayMode === 'average'">
<div class="left average" style="width: 40%;">
<div v-for="card in cards.slice(0, 3)" :key="card.id" class="card digital-twin-card--deep-blue"
@click="handleSelectCard(card.id)">
<div 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">{{ card.title }}</span>
</div>
<div v-if="card.type === 'overview'" class="time-range-selector">
<el-date-picker v-if="historyTimeGranularity === 'year'" type="yearrange" range-separator=""
start-placeholder="开始年" end-placeholder="结束年" format="YYYY" value-format="YYYY"
class="date-range-picker" @click.stop />
<el-date-picker v-else-if="historyTimeGranularity === 'month'" type="monthrange" range-separator=""
start-placeholder="开始月" end-placeholder="结束月" format="YYYY-MM" value-format="YYYY-MM"
class="date-range-picker" @click.stop />
<!-- <el-date-picker v-else-if="historyTimeGranularity === 'week'" type="weekrange" range-separator=""
start-placeholder="开始周" end-placeholder="结束周" format="YYYY-MM-DD" value-format="YYYY-MM-DD"
class="date-range-picker" @click.stop /> -->
<el-date-picker v-else v-model="dateRange" type="daterange" range-separator="" start-placeholder="开始日"
end-placeholder="结束日" format="YYYY-MM-DD" value-format="YYYY-MM-DD" class="date-range-picker" />
<button v-for="option in historyTimeGranularityOptions" :key="option.value"
:class="['time-range-btn', { active: historyTimeGranularity === option.value }]"
@click.stop="handleHistoryTimeGranularityChange(option.value as 'year' | 'month' | 'week' | 'day')">
{{ option.label }}
</button>
<el-button type="primary" @click.stop="handleDateRangeConfirm">选择并确认</el-button>
</div>
<div v-if="card.type === 'chart'" class="show-value">
<span class="show-value-label">{{timeRangeOptions.find(option => option.value === timeRange)?.label ||
''}}:</span>
<div class="show-value-value">{{ chartData[timeRange][card.value] }}</div>
</div>
</div>
<div class="card-content">
<div v-if="card.type === 'overview'" class="overview-grid">
<div class="overview-item">
<div class="overview-value">{{ chartData[timeRange].totalPower }}</div>
<div class="overview-label">累计用电千瓦时</div>
</div>
<div class="overview-item">
<div class="overview-value">{{ chartData[timeRange].fuel }}</div>
<div class="overview-label">减少燃油</div>
</div>
<div class="overview-item">
<div class="overview-value">{{ chartData[timeRange].co2 }}</div>
<div class="overview-label">减少二氧化碳排放千克</div>
</div>
<div class="overview-item">
<div class="overview-value">{{ chartData[timeRange].pm25 }}</div>
<div class="overview-label">减少PM2.5排放千克</div>
</div>
<div class="overview-item">
<div class="overview-value">{{ chartData[timeRange].nox }}</div>
<div class="overview-label">减少氮氧化物千克</div>
</div>
<div class="overview-item">
<div class="overview-value">{{ chartData[timeRange].so2 }}</div>
<div class="overview-label">减少二氧化硫千克</div>
</div>
</div>
<WaveLineChart v-else :chart-data="getChartData(card.id)" :title="card.title" :color="card.color" />
</div>
</div>
</div>
<div class="right average" style="width: 40%;">
<div v-for="card in cards.slice(3)" :key="card.id" class="card digital-twin-card--deep-blue"
@click="handleSelectCard(card.id)">
<div 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">{{ card.title }}</span>
</div>
<div v-if="card.type === 'overview'" class="time-range-selector">
<el-date-picker v-if="historyTimeGranularity === 'year'" type="yearrange" range-separator=""
start-placeholder="开始年" end-placeholder="结束年" format="YYYY" value-format="YYYY"
class="date-range-picker" @click.stop />
<el-date-picker v-else-if="historyTimeGranularity === 'month'" type="monthrange" range-separator=""
start-placeholder="开始月" end-placeholder="结束月" format="YYYY-MM" value-format="YYYY-MM"
class="date-range-picker" @click.stop />
<!-- <el-date-picker v-else-if="historyTimeGranularity === 'week'" type="weekrange" range-separator=""
start-placeholder="开始周" end-placeholder="结束周" format="YYYY-MM-DD" value-format="YYYY-MM-DD"
class="date-range-picker" @click.stop /> -->
<el-date-picker v-else type="daterange" range-separator="" start-placeholder="开始日"
end-placeholder="结束日" format="YYYY-MM-DD" value-format="YYYY-MM-DD" class="date-range-picker"
@click.stop />
<button v-for="option in historyTimeGranularityOptions" :key="option.value"
:class="['time-range-btn', { active: historyTimeGranularity === option.value }]"
@click.stop="handleHistoryTimeGranularityChange(option.value as 'year' | 'month' | 'week' | 'day')">
{{ option.label }}
</button>
<el-button type="primary" @click.stop="handleDateRangeConfirm">选择并确认</el-button>
</div>
<div v-if="card.type === 'chart'" class="show-value">
<span class="show-value-label">{{timeRangeOptions.find(option => option.value === timeRange)?.label ||
''}}:</span>
<div class="show-value-value">{{ chartData[timeRange][card.value] }}</div>
</div>
</div>
<div class="card-content">
<div v-if="card.type === 'overview'" class="overview-grid">
<div class="overview-item">
<div class="overview-value">{{ totalPower }}</div>
<div class="overview-label">累计用电千瓦时</div>
</div>
<div class="overview-item">
<div class="overview-value">{{ fuelReduction }}</div>
<div class="overview-label">减少燃油</div>
</div>
<div class="overview-item">
<div class="overview-value">{{ co2Reduction }}</div>
<div class="overview-label">减少二氧化碳排放千克</div>
</div>
<div class="overview-item">
<div class="overview-value">{{ pm25Reduction }}</div>
<div class="overview-label">减少PM2.5排放千克</div>
</div>
<div class="overview-item">
<div class="overview-value">{{ noxReduction }}</div>
<div class="overview-label">减少氮氧化物千克</div>
</div>
<div class="overview-item">
<div class="overview-value">{{ so2Reduction }}</div>
<div class="overview-label">减少二氧化硫千克</div>
</div>
</div>
<WaveLineChart v-else :chart-data="getChartData(card.id)" :title="card.title" :color="card.color" />
</div>
</div>
</div>
</template>
<!-- Magnify Mode -->
<div v-if="displayMode === 'magnify'" class="magnify">
<div class="magnify">
<div class="big">
<div v-for="card in cards.filter(c => c.id === selectedCard)" :key="card.id"
class="card digital-twin-card--deep-blue">
@ -148,18 +12,29 @@
<span class="title-text">{{ card.title }}</span>
</div>
<div v-if="card.type === 'overview'" class="time-range-selector">
<el-button type="primary" @click.stop="pageType = 'history'">历史查询</el-button>
<div class="time-range-item">起始时间: {{ startTimeDisplay }}</div>
<div class="time-range-item">更新时间: {{ realtimeDeviceDataTime }}</div>
<button v-for="option in timeRangeOptions" :key="option.value"
:class="['time-range-btn', { active: timeRange === option.value }]"
@click.stop="handleTimeRangeChange(option.value)">
{{ option.label }}
</button>
<!-- 返回上个页面 -->
<el-button type="primary" @click.stop="pageType = 'history'">历史查询</el-button>
<el-button type="primary" @click.stop="handleGoBack()">返回</el-button>
</div>
<div v-if="card.type === 'chart'" class="show-value">
<div class="time-range-item">起始时间: {{ startTimeDisplay }}</div>
<div class="time-range-item">更新时间: {{ realtimeDeviceDataTime }}</div>
<span class="show-value-label">{{timeRangeOptions.find(option => option.value === timeRange)?.label ||
''}}:</span>
<div class="show-value-value">{{ chartData[timeRange][card.value] }}</div>
<el-button type="primary" @click.stop="handleGoBack()">返回</el-button>
</div>
</div>
<div class="card-content">
<div v-if="card.type === 'overview'" class="overview-grid">
@ -249,121 +124,7 @@
</div>
</template>
<template v-if="pageType === 'history'">
<!-- Average Mode -->
<template v-if="displayMode === 'average'">
<div class="left average" style="width: 40%;">
<div v-for="card in cards.slice(0, 3)" :key="card.id" class="card digital-twin-card--deep-blue"
@click="handleSelectCard(card.id)">
<div 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">{{ card.title }}</span>
</div>
<div v-if="card.type === 'overview'" class="time-range-selector">
<button v-for="option in historyTimeGranularityOptions" :key="option.value"
:class="['time-range-btn', { active: historyTimeGranularity === option.value }]"
@click.stop="handleHistoryTimeGranularityChange(option.value as 'year' | 'month' | 'week' | 'day')">
{{ option.label }}
</button>
<el-button type="primary" @click.stop="handleDateRangeConfirm">选择并确认</el-button>
</div>
<div v-if="card.type === 'chart'" class="show-value">
<span class="show-value-label">{{timeRangeOptions.find(option => option.value === timeRange)?.label ||
''}}:</span>
<div class="show-value-value">{{ chartData[timeRange][card.value] }}</div>
</div>
</div>
<div class="card-content">
<div v-if="card.type === 'overview'" class="overview-grid">
<div class="overview-item">
<div class="overview-value">{{ chartData[timeRange].totalPower }}</div>
<div class="overview-label">累计用电千瓦时</div>
</div>
<div class="overview-item">
<div class="overview-value">{{ chartData[timeRange].fuel }}</div>
<div class="overview-label">减少燃油</div>
</div>
<div class="overview-item">
<div class="overview-value">{{ chartData[timeRange].co2 }}</div>
<div class="overview-label">减少二氧化碳排放千克</div>
</div>
<div class="overview-item">
<div class="overview-value">{{ chartData[timeRange].pm25 }}</div>
<div class="overview-label">减少PM2.5排放千克</div>
</div>
<div class="overview-item">
<div class="overview-value">{{ chartData[timeRange].nox }}</div>
<div class="overview-label">减少氮氧化物千克</div>
</div>
<div class="overview-item">
<div class="overview-value">{{ chartData[timeRange].so2 }}</div>
<div class="overview-label">减少二氧化硫千克</div>
</div>
</div>
<WaveLineChart v-else :chart-data="getChartData(card.id)" :title="card.title" :color="card.color" />
</div>
</div>
</div>
<div class="right average" style="width: 40%;">
<div v-for="card in cards.slice(3)" :key="card.id" class="card digital-twin-card--deep-blue"
@click="handleSelectCard(card.id)">
<div 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">{{ card.title }}</span>
</div>
<div v-if="card.type === 'overview'" class="time-range-selector">
<el-date-picker type="daterange" range-separator="" start-placeholder="开始日期" end-placeholder="结束日期"
format="YYYY-MM-DD" value-format="YYYY-MM-DD" class="date-range-picker" @click.stop />
<button v-for="option in timeRangeOptions" :key="option.value"
:class="['time-range-btn', { active: timeRange === option.value }]"
@click.stop="handleTimeRangeChange(option.value)">
{{ option.label }}
</button>
</div>
<div v-if="card.type === 'chart'" class="show-value">
<span class="show-value-label">{{timeRangeOptions.find(option => option.value === timeRange)?.label ||
''}}:</span>
<div class="show-value-value">{{ chartData[timeRange][card.value] }}</div>
</div>
</div>
<div class="card-content">
<div v-if="card.type === 'overview'" class="overview-grid">
<div class="overview-item">
<div class="overview-value">{{ totalPower }}</div>
<div class="overview-label">累计用电千瓦时</div>
</div>
<div class="overview-item">
<div class="overview-value">{{ fuelReduction }}</div>
<div class="overview-label">减少燃油</div>
</div>
<div class="overview-item">
<div class="overview-value">{{ co2Reduction }}</div>
<div class="overview-label">减少二氧化碳排放千克</div>
</div>
<div class="overview-item">
<div class="overview-value">{{ pm25Reduction }}</div>
<div class="overview-label">减少PM2.5排放千克</div>
</div>
<div class="overview-item">
<div class="overview-value">{{ noxReduction }}</div>
<div class="overview-label">减少氮氧化物千克</div>
</div>
<div class="overview-item">
<div class="overview-value">{{ so2Reduction }}</div>
<div class="overview-label">减少二氧化硫千克</div>
</div>
</div>
<WaveLineChart v-else :chart-data="getChartData(card.id)" :title="card.title" :color="card.color" />
</div>
</div>
</div>
</template>
<!-- Magnify Mode -->
<div v-if="displayMode === 'magnify'" class="magnify">
<div class="magnify">
<div class="big">
<div v-for="card in cards.filter(c => c.id === selectedCard)" :key="card.id"
class="card digital-twin-card--deep-blue">
@ -374,18 +135,30 @@
<span class="title-text">{{ card.title }}</span>
</div>
<div v-if="card.type === 'overview'" class="time-range-selector">
<el-date-picker v-if="historyTimeGranularity === 'year'" v-model="dateRange" type="yearrange"
range-separator="至" start-placeholder="开始年" end-placeholder="结束年" format="YYYY" value-format="YYYY"
class="date-range-picker" @change="handleSelectDate" />
<el-date-picker v-else-if="historyTimeGranularity === 'month'" v-model="dateRange" type="monthrange"
range-separator="至" start-placeholder="开始月" end-placeholder="结束月" format="YYYY-MM"
value-format="YYYY-MM" class="date-range-picker" @change="handleSelectDate" />
<!-- <el-date-picker v-else-if="historyTimeGranularity === 'week'" v-model="dateRange" type="week"
range-separator="至" start-placeholder="开始周" end-placeholder="结束周" format="YYYY-MM-DD"
value-format="YYYY-MM-DD" class="date-range-picker" @change="handleSelectDate" /> -->
<el-date-picker v-else v-model="dateRange" type="daterange" range-separator="" start-placeholder="开始日"
end-placeholder="结束日" format="YYYY-MM-DD" value-format="YYYY-MM-DD" class="date-range-picker"
@change="handleSelectDate" />
<div class="separate-date-pickers">
<div class="date-picker-group">
<span class="date-label">开始时间:</span>
<el-date-picker v-if="historyTimeGranularity === 'year'" v-model="startDate" type="year"
placeholder="开始年" format="YYYY" value-format="YYYY" class="date-picker"
@change="handleStartDateChange" />
<el-date-picker v-else-if="historyTimeGranularity === 'month'" v-model="startDate" type="month"
placeholder="开始月" format="YYYY-MM" value-format="YYYY-MM" class="date-picker"
@change="handleStartDateChange" />
<el-date-picker v-else v-model="startDate" type="date" placeholder="开始日" format="YYYY-MM-DD"
value-format="YYYY-MM-DD" class="date-picker" @change="handleStartDateChange" />
</div>
<div class="date-picker-group">
<span class="date-label">结束时间:</span>
<el-date-picker v-if="historyTimeGranularity === 'year'" v-model="endDate" type="year"
placeholder="结束年" format="YYYY" value-format="YYYY" class="date-picker"
@change="handleEndDateChange" />
<el-date-picker v-else-if="historyTimeGranularity === 'month'" v-model="endDate" type="month"
placeholder="结束月" format="YYYY-MM" value-format="YYYY-MM" class="date-picker"
@change="handleEndDateChange" />
<el-date-picker v-else v-model="endDate" type="date" placeholder="结束日" format="YYYY-MM-DD"
value-format="YYYY-MM-DD" class="date-picker" @change="handleEndDateChange" />
</div>
</div>
<button v-for="option in historyTimeGranularityOptions" :key="option.value"
:class="['time-range-btn', { active: historyTimeGranularity === option.value }]"
@click.stop="handleHistoryTimeGranularityChange(option.value as 'year' | 'month' | 'week' | 'day')">
@ -399,9 +172,11 @@
</button> -->
</div>
<div v-if="card.type === 'chart'" class="show-value">
<span class="show-value-label">{{timeRangeOptions.find(option => option.value === timeRange)?.label ||
''}}:</span>
<div class="show-value-value">{{ chartData[timeRange][card.value] }}</div>
<div class="time-range-item">记录时间范围: {{ selectedDateRangeDisplay }}</div>
<!-- <div class="time-range-item">{{ realtimeDeviceDataTime }}</div> -->
<!-- <span class="show-value-label">{{timeRangeOptions.find(option => option.value === timeRange)?.label ||
''}}:</span> -->
<!-- <div class="show-value-value">{{ chartData[timeRange][card.value] }}</div> -->
</div>
</div>
<div class="card-content">
@ -453,9 +228,8 @@
</button> -->
</div>
<div v-if="card.type === 'chart'" class="show-value">
<span class="show-value-label">{{timeRangeOptions.find(option => option.value === timeRange)?.label ||
''}}:</span>
<div class="show-value-value">{{ chartData[timeRange][card.value] }}</div>
<span class="show-value-label">区间量</span>
<div class="show-value-value">{{ historyData[card.value] }}</div>
</div>
</div>
<div class="card-content">
@ -492,11 +266,12 @@
</div>
</div>
</template>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
import WaveLineChart from './charts/WaveLineChart.vue'
import { MapApi } from "@/api/shorepower/map";
import dayjs from 'dayjs';
@ -507,6 +282,12 @@ import { formatTimestamp, parseRangeToTimestamp } from './utils';
interface Props {
realtimeDeviceData: RealtimeDeviceData[];
activeHeadGroup?: number;
handleGoBack: () => void;
realtimeDeviceDataTime: string;
initialTimeRange?: string;
yearData: RealtimeDeviceData[];
monthData: RealtimeDeviceData[];
dayData: RealtimeDeviceData[];
}
const props = defineProps<Props>()
@ -525,6 +306,11 @@ interface CardInfo {
value: string;
}
interface RawItem {
measureValue: number;
measureTime: Date;
}
interface ShorePowerUsageData {
totalPower: number; //
fuelReduction: number; //
@ -585,6 +371,8 @@ export interface deviceData {
const pageType = ref<'realtime' | 'history'>('realtime')
const dateRange = ref<string[]>([])
const startDate = ref<string>('')
const endDate = ref<string>('')
const totalPower = ref<string>('0')
const fuelReduction = ref<string>('0')
const co2Reduction = ref<string>('0')
@ -592,6 +380,68 @@ const pm25Reduction = ref<string>('0')
const noxReduction = ref<string>('0')
const so2Reduction = ref<string>('0')
//
const startTimeDisplay = computed(() => {
const now = new Date();
let startDate: Date;
switch (timeRange.value) {
case 'day':
//
startDate = new Date(now.getFullYear(), now.getMonth(), now.getDate());
break;
case 'month':
//
startDate = new Date(now.getFullYear(), now.getMonth(), 1);
break;
case 'year':
//
startDate = new Date(now.getFullYear(), 0, 1);
break;
case 'realtime':
// 2020
startDate = new Date(2020, 0, 1);
break;
default:
//
startDate = new Date(now.getFullYear(), now.getMonth(), now.getDate());
}
//
const year = startDate.getFullYear();
const month = String(startDate.getMonth() + 1).padStart(2, '0');
const day = String(startDate.getDate()).padStart(2, '0');
const hours = String(startDate.getHours()).padStart(2, '0');
const minutes = String(startDate.getMinutes()).padStart(2, '0');
const seconds = String(startDate.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
});
//
const selectedDateRangeDisplay = computed(() => {
/* if (!dateRange.value || dateRange.value.length !== 2) {
return '';
} */
// const [startDate, endDate] = dateRange.value;
// const startDate = startDate.value
// const endDate = endDate.value
//
switch (historyTimeGranularity.value) {
case 'year':
// : YYYY
return `${startDate.value} - ${endDate.value}`;
case 'month':
// : YYYY-MM
return `${startDate.value} - ${endDate.value}`;
default:
// : YYYY-MM-DD
return `${startDate.value} - ${endDate.value}`;
}
});
const displayMode = ref<'average' | 'magnify'>('magnify')
const selectedCard = ref<string>('overview')
//
@ -717,6 +567,64 @@ const calculateTotalDiff = (data) => {
}, 0);
}
//
const handleStartDateChange = (date: string | null) => {
if (date) {
startDate.value = date
//
if (!endDate.value) {
const start = new Date(date)
let end = new Date(date)
switch (historyTimeGranularity.value) {
case 'year':
end.setFullYear(start.getFullYear() + 1)
break
case 'month':
end.setMonth(start.getMonth() + 1)
break
default:
end.setDate(start.getDate() + 7) //
break
}
//
const formatEndDate = (date: Date) => {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
switch (historyTimeGranularity.value) {
case 'year':
return year.toString()
case 'month':
return `${year}-${month}`
default:
return `${year}-${month}-${day}`
}
}
endDate.value = formatEndDate(end)
}
//
if (startDate.value && endDate.value) {
handleSelectDate([startDate.value, endDate.value])
}
}
}
//
const handleEndDateChange = (date: string | null) => {
if (date) {
endDate.value = date
//
if (startDate.value && endDate.value) {
handleSelectDate([startDate.value, endDate.value])
}
}
}
const handleSelectDate = async (range: string[] | null) => {
if (!range || range.length !== 2) {
//
@ -849,16 +757,16 @@ const getHistoryChartData = (cardId: string): ChartDataItem[] => {
}
//
const timeRange = ref<'realtime' | 'day' | 'week' | 'month' | 'quarter' | 'year'>('realtime')
const timeRange = ref<'realtime' | 'day' | 'month' | 'quarter' | 'year'>(
(props.initialTimeRange as 'realtime' | 'day' | 'month' | 'quarter' | 'year') || 'day'
)
//
const timeRangeOptions = [
{ value: 'realtime', label: '总' },
{ value: 'day', label: '本日' },
{ value: 'week', label: '本周' },
{ value: 'month', label: '本月' },
// { value: 'quarter', label: '' },
{ value: 'year', label: '本年' }
{ value: 'day', label: '当日' },
{ value: 'month', label: '当月' },
{ value: 'year', label: '当年' },
{ value: 'realtime', label: '汇总' },
]
//
@ -983,13 +891,12 @@ const handleGetRealTimeAllData = async (realtimeDeviceData: RealtimeDeviceData[]
try {
const res: RealtimeDeviceData[] = realtimeDeviceData.filter(item => item.deviceCode.includes('Kwh'));
const ids = res.map(item => item.id);
console.log(ids);
const params = {
ids: ids.join(','),
// year: new Date().getFullYear()
}
if (!params.ids) return;
let yearDataRes: RealtimeDeviceData[] = []
/* let yearDataRes: RealtimeDeviceData[] = []
let weekDataRes: RealtimeDeviceData[] = []
let monthDataRes: RealtimeDeviceData[] = []
let dayDataRes: RealtimeDeviceData[] = []
@ -1001,16 +908,16 @@ const handleGetRealTimeAllData = async (realtimeDeviceData: RealtimeDeviceData[]
dayDataRes = await MapApi.getDayDataByIdList(params);
} catch (error) {
console.log(error);
}
} */
// const quarterDataRes: deviceData = await MapApi.getQuarterDataByIdList(params);
const realTimeSum = res.reduce((acc, item) => acc + item.measureValue, 0);
const daySum = realTimeSum - Object.values(dayDataRes).reduce((acc, item) => acc + item.measureValue, 0);
const weekSum = realTimeSum - Object.values(weekDataRes).reduce((acc, item) => acc + item.measureValue, 0);
const monthSum = realTimeSum - Object.values(monthDataRes).reduce((acc, item) => acc + item.measureValue, 0);
const daySum = realTimeSum - Object.values(props.dayData).reduce((acc, item) => acc + item.measureValue, 0);
// const weekSum = realTimeSum - Object.values(weekDataRes).reduce((acc, item) => acc + item.measureValue, 0);
const monthSum = realTimeSum - Object.values(props.monthData).reduce((acc, item) => acc + item.measureValue, 0);
// const quarterSum = Object.values(quarterDataRes).reduce((acc, item) => acc + item.measureValue, 0);
const yearSum = realTimeSum - Object.values(yearDataRes).reduce((acc, item) => acc + item.measureValue, 0);
const yearSum = realTimeSum - Object.values(props.yearData).reduce((acc, item) => acc + item.measureValue, 0);
// totalPower.value = realTimeSum.toFixed(2)
// co2Reduction.value = (realTimeSum * 670 / 1000000).toFixed(2) //
@ -1039,14 +946,14 @@ const handleGetRealTimeAllData = async (realtimeDeviceData: RealtimeDeviceData[]
nox: Number((daySum * 18.1 / 1000).toFixed(2)), //
so2: Number((daySum * 10.5 / 1000).toFixed(2)), //
}
chartData.value.week = {
/* chartData.value.week = {
totalPower: Number(weekSum.toFixed(2)),
fuel: Number((weekSum * 0.22 / 1).toFixed(2)), //
co2: Number((weekSum * 670 / 1000).toFixed(2)), //
pm25: Number((weekSum * 1.46 / 1000).toFixed(2)), //
nox: Number((weekSum * 18.1 / 1000).toFixed(2)), //
so2: Number((weekSum * 10.5 / 1000).toFixed(2)), //
}
} */
chartData.value.month = {
totalPower: Number(monthSum.toFixed(2)),
fuel: Number((monthSum * 0.22 / 1).toFixed(2)), //
@ -1124,9 +1031,18 @@ watch(() => props.realtimeDeviceData, (newValue) => {
})
//
//
let cardSelectedHandler: any = null
onMounted(() => {
// handleGetAllDeviceDataByTimeRange()
handleGetRealTimeAllData(props.realtimeDeviceData)
//
/* cardSelectedHandler = (event: CustomEvent) => {
selectedCard.value = event.detail
}
window.addEventListener('cardSelected', cardSelectedHandler) */
})
// activeHeadGroup resize
@ -1139,9 +1055,19 @@ watch(() => props.activeHeadGroup, (newVal) => {
}
}, { immediate: true })
// initialTimeRange
watch(() => props.initialTimeRange, (newVal) => {
console.log('newVal', newVal)
if (newVal) {
timeRange.value = newVal as 'realtime' | 'day' | 'month' | 'year'
}
})
//
onBeforeUnmount(() => {
if (cardSelectedHandler) {
window.removeEventListener('cardSelected', cardSelectedHandler)
}
})
</script>
@ -1218,6 +1144,7 @@ onBeforeUnmount(() => {
.time-range-selector {
display: flex;
align-items: center;
gap: 8px;
}
@ -1233,37 +1160,45 @@ onBeforeUnmount(() => {
}
.show-value-label {
font-size: 12px;
font-size: 18px;
font-weight: 600;
font-weight: bold;
}
.show-value-value {
font-size: 18px;
font-size: 20px;
font-weight: 600;
font-weight: bold;
}
.time-range-btn {
padding: 4px 12px;
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 4px;
background: transparent;
.time-range-item {
font-size: 24px;
font-weight: 600;
font-weight: bold;
color: rgba(255, 255, 255, 0.7);
font-size: 12px;
cursor: pointer;
transition: all 0.3s ease;
}
.time-range-btn:hover {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.5);
.separate-date-pickers {
display: flex;
align-items: center;
gap: 12px;
}
.date-picker-group {
display: flex;
align-items: center;
gap: 8px;
}
.date-label {
font-size: 14px;
font-weight: 500;
color: rgba(255, 255, 255, 0.8);
white-space: nowrap;
}
.time-range-btn.active {
background: rgba(76, 175, 80, 0.8);
border-color: #4CAF50;
color: #fff;
.date-picker {
width: 140px;
}
.right {

468
public/map/components/ShorePowerUsageRate.vue

@ -0,0 +1,468 @@
<template>
<div class="shore-power-usage">
<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>
<div class="show-value">
<button v-for="option in timeRangeOptions" :key="option.value"
:class="['time-range-btn', { active: timeRange === option.value }]"
@click.stop="handleTimeRangeChange(option.value)">
{{ option.label }}
</button>
<!-- <span class="show-value-label">{{timeRangeOptions.find(option => option.value === timeRange)?.label ||
''}}:</span>
<div class="show-value-value">{{ chartData[timeRange][card.value] }}</div> -->
<el-button class="close-btn" size="small" type="text" @click.stop="handleClose()">x</el-button>
</div>
</div>
<div style="display: flex; align-items: center; justify-content: space-between;">
<div class="time-range-item">{{ startTimeDisplay }} {{ realtimeDeviceDataTime }}</div>
<div v-show="timeRange !== 'year'" class="comparison-type-selector">
<button :class="['comparison-type-btn', { active: comparisonType === 'chain' }]"
@click="comparisonType = 'chain'">
环比
</button>
<button :class="['comparison-type-btn', { active: comparisonType === 'yearOnYear' }]"
@click="comparisonType = 'yearOnYear'">
同比
</button>
</div>
</div>
<div v-if="timeRange === 'year'" class="card-content">
<ComparisonBarChart :chartData="getYearlyData" title="年度用电量对比" yAxisName="用电量(kWh)" currentColor="#36A2EB"
previousColor="#FF6384" chartType="percentage" />
</div>
<div v-if="timeRange === 'month'" class="card-content">
<ComparisonBarChart :chartData="getMonthlyComparisonData"
:title="comparisonType === 'yearOnYear' ? '月度用电量同比对比' : '月度用电量环比对比'" yAxisName="用电量(kWh)"
currentColor="#36A2EB" previousColor="#FF6384" chartType="percentage" />
</div>
<div class="card-content" v-if="timeRange === 'day'">
<ComparisonBarChart :chartData="getDailyComparisonData"
:title="comparisonType === 'yearOnYear' ? '日用电量同比对比' : '日用电量环比对比'" yAxisName="用电量(kWh)" currentColor="#36A2EB"
previousColor="#FF6384" chartType="percentage" />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
import { MapApi } from "@/api/shorepower/map";
import ComparisonBarChart from './charts/ComparisonBarChart.vue'
import { ComparativeData, MetricData, RealtimeDeviceData } from '@/types/shorepower';
interface Props {
// realtimeDeviceData: RealtimeDeviceData[];
// activeHeadGroup?: number;
handleClose: () => void;
realtimeDeviceDataTime: string;
comparativeData: MetricData;
initialTimeRange?: 'realtime' | 'day' | 'month' | 'year';
}
const props = defineProps<Props>()
export interface MeasureDataRealtimeRespVO {
/**
* 创建时间
*/
createTime: Date;
/**
* 设备编号code
*/
deviceCode: string;
/**
* 设备编号
*/
deviceId: number;
/**
* 设备名称
*/
deviceName: string;
/**
* 设备状态
*/
deviceStatus: number;
/**
* 编号
*/
id: number;
/**
* 增量费用
*/
incrementCost?: number;
/**
* 增量值
*/
incrementValue: number;
/**
* 采集时间
*/
measureTime: Date;
/**
* 采集值
*/
measureValue: number;
}
export interface deviceData {
[key: string]: MeasureDataRealtimeRespVO
}
const getYearlyData = computed(() => {
return [{
name: '本期上期对比',
growthRate: props.comparativeData.year.growthRate,
currentValue: convertPowerUsage(props.comparativeData.year.current.value, selectedCard.value),
previousValue: convertPowerUsage(props.comparativeData.year.previous.value, selectedCard.value),
currentPeriod: props.comparativeData.year.current.period,
previousPeriod: props.comparativeData.year.previous.period,
}]
})
//
const getDailyComparisonData = computed(() => {
const comparisonData = comparisonType.value === 'yearOnYear'
? props.comparativeData.dayYearOnYear
: props.comparativeData.day;
return [{
name: comparisonType.value === 'yearOnYear' ? '本期去年同期对比' : '本期上期对比',
growthRate: comparisonData.growthRate,
currentValue: convertPowerUsage(comparisonData.current.value, selectedCard.value),
previousValue: convertPowerUsage(comparisonData.previous.value, selectedCard.value),
currentPeriod: comparisonData.current.period,
previousPeriod: comparisonData.previous.period,
}]
})
//
const getMonthlyComparisonData = computed(() => {
const comparisonData = comparisonType.value === 'yearOnYear'
? props.comparativeData.monthYearOnYear
: props.comparativeData.month;
return [{
name: comparisonType.value === 'yearOnYear' ? '本期去年同期对比' : '本期上期对比',
growthRate: comparisonData.growthRate,
currentValue: convertPowerUsage(comparisonData.current.value, selectedCard.value),
previousValue: convertPowerUsage(comparisonData.previous.value, selectedCard.value),
currentPeriod: comparisonData.current.period,
previousPeriod: comparisonData.previous.period,
}]
})
//
const startTimeDisplay = computed(() => {
const now = new Date();
let startDate: Date;
switch (timeRange.value) {
case 'day':
//
startDate = new Date(now.getFullYear(), now.getMonth(), now.getDate());
break;
case 'month':
//
startDate = new Date(now.getFullYear(), now.getMonth(), 1);
break;
case 'year':
//
startDate = new Date(now.getFullYear(), 0, 1);
break;
/* case 'realtime':
// 2020
startDate = new Date(2020, 0, 1);
break; */
default:
//
startDate = new Date(now.getFullYear(), now.getMonth(), now.getDate());
}
//
const year = startDate.getFullYear();
const month = String(startDate.getMonth() + 1).padStart(2, '0');
const day = String(startDate.getDate()).padStart(2, '0');
const hours = String(startDate.getHours()).padStart(2, '0');
const minutes = String(startDate.getMinutes()).padStart(2, '0');
const seconds = String(startDate.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
});
const selectedCard = ref<string>('overview')
//
const timeRange = ref<'day' | 'month' | 'year'>(
(props.initialTimeRange === 'realtime' ? 'year' : props.initialTimeRange) || 'day'
)
// chain yearOnYear
const comparisonType = ref<'chain' | 'yearOnYear'>('chain')
//
const timeRangeOptions = [
{ value: 'day', label: '当日' },
{ value: 'month', label: '当月' },
{ value: 'year', label: '当年' },
// { value: 'realtime', label: '' },
]
//
const handleTimeRangeChange = (range: 'day' | 'month' | 'year') => {
timeRange.value = range
//
}
//
const convertPowerUsage = (powerUsage: number, type: string): number => {
const conversionFactors: Record<string, number> = {
totalPower: 1, // 使
fuel: 0.22 / 1, //
co2: 670 / 1000, //
pm25: 1.46 / 1000, //
nox: 18.1 / 1000, //
so2: 10.5 / 1000 //
};
const factor = conversionFactors[type] || 1;
return Number((powerUsage * factor).toFixed(2));
};
/* watch(() => props.realtimeDeviceData, (newValue) => {
handleGetRealTimeAllData(newValue)
}) */
//
//
let cardSelectedHandler: any = null
onMounted(() => {
// handleGetAllDeviceDataByTimeRange()
// handleGetRealTimeAllData(props.realtimeDeviceData)
//
// cardSelectedHandler = (event: CustomEvent) => {
// selectedCard.value = event.detail
// }
// window.addEventListener('cardSelected', cardSelectedHandler)
})
// activeHeadGroup resize
watch(() => props.activeHeadGroup, (newVal) => {
if (newVal === 1) {
// resize DOM
setTimeout(() => {
window.dispatchEvent(new Event('resize'))
}, 100)
}
}, { immediate: true })
// initialTimeRange
watch(() => props.initialTimeRange, (newVal) => {
console.log('newVal', newVal)
if (newVal) {
if (newVal === 'realtime') {
timeRange.value = 'year'
} else {
timeRange.value = newVal as 'day' | 'month' | 'year'
}
}
})
//
onBeforeUnmount(() => {
if (cardSelectedHandler) {
window.removeEventListener('cardSelected', cardSelectedHandler)
}
})
</script>
<style lang="scss" scoped>
.shore-power-usage {
display: flex;
height: 100%;
width: 100%;
gap: 10px;
}
.card .card-content {
overflow: hidden !important;
}
.average {
.overview-value {
font-size: 36px;
}
.overview-label {
font-size: 18px;
}
}
.magnify {
width: 100%;
padding: 12px;
height: calc(100vh - 72px);
position: absolute;
top: 72px;
left: 0px;
display: flex;
// flex-direction: column;
/* background-color: red; */
gap: 8px;
overflow-y: auto;
.big {
height: 100%;
width: calc(55%);
.card {
height: 100%;
}
.card .card-title .title-text {
width: 120px;
}
.overview-value {
font-size: 48px;
}
.overview-label {
font-size: 28px;
}
}
.right-row {
width: calc(45% - 8px);
height: 100%;
display: flex;
flex-direction: column;
flex: 1;
gap: 8px;
// background-color: red;
}
.one-row {
height: 30%;
display: flex;
gap: 8px;
.card .card-title .title-text {
font-size: 14px;
}
.card {
flex: 1;
}
}
}
.card {
padding: 6px;
cursor: pointer;
}
.time-range-selector {
display: flex;
align-items: center;
gap: 8px;
}
.show-value {
font-size: 18px;
font-weight: 600;
font-weight: bold;
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 8px;
color: #FFF;
}
.show-value-label {
font-size: 18px;
font-weight: 600;
font-weight: bold;
}
.show-value-value {
font-size: 20px;
font-weight: 600;
font-weight: bold;
}
.time-range-item {
font-size: 24px;
font-weight: 600;
font-weight: bold;
color: rgba(255, 255, 255, 0.7);
}
.growth-rate {
flex: 1;
text-align: right;
margin-left: 10px;
font-weight: bold;
padding: 2px 8px;
border-radius: 4px;
font-size: 32px;
}
.growth-up {
color: #52C41A;
}
.growth-down {
color: #F5222D;
}
.growth-icon {
margin-right: 4px;
}
.right {
/* gap: 0 */
}
.close-btn {
width: 24px;
height: 24px;
// border-radius: 50%;
// background-color: #ff4d4f;
color: white;
border: none;
font-weight: bold;
font-size: 16px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
margin-left: 10px;
// margin-bottom: 8px;
}
.close-btn:hover {
color: #f5222d;
}
</style>

1197
public/map/components/ShorePowerUsageSingleData.vue

File diff suppressed because it is too large

1341
public/map/components/ShowData.vue

File diff suppressed because it is too large

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

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

890
public/map/components/cesiumMap.vue

File diff suppressed because it is too large

196
public/map/components/charts/ComparisonBarChart.vue

@ -0,0 +1,196 @@
<template>
<div ref="chartContainer" class="comparison-bar-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
currentValue: number
previousValue: number
growthRate?: number
currentPeriod?: string
previousPeriod?: string
}>
title?: string
currentColor?: string
previousColor?: string
showYAxis?: boolean
yAxisName?: string
currentLabel?: string
previousLabel?: string
chartType?: 'default' | 'percentage'
}
const props = withDefaults(defineProps<Props>(), {
chartData: () => [],
title: '',
currentColor: '#1890FF',
previousColor: '#A9A9A9',
showYAxis: true,
yAxisName: '数值',
currentLabel: '本期',
previousLabel: '上期',
chartType: 'default'
})
//
const chartContainer = ref<HTMLDivElement | null>(null)
let chartInstance: echarts.ECharts | null = null
//
const createBarOption = (currentLabel: string, previousLabel: string, currentValue: number, previousValue: number) => {
//
const isPercentage = props.chartType === 'percentage';
return {
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
formatter: (params: any) => {
const value = params[0].value;
const formattedValue = isPercentage ? `${(value * 100).toFixed(1)}%` : value.toFixed(2);
return `${params[0].name}: ${formattedValue}`;
}
},
grid: {
left: '15%',
right: '10%',
bottom: '20%',
top: '20%'
},
xAxis: {
type: 'category',
data: [previousLabel, currentLabel],
axisTick: { show: false },
axisLine: { show: false },
axisLabel: {
color: 'rgba(255, 255, 255, 0.7)',
fontSize: 22
}
},
yAxis: {
type: 'value',
axisLabel: {
formatter: isPercentage ? (value: number) => `${(value * 100).toFixed(0)}%` : '{value}',
color: 'rgba(255, 255, 255, 0.7)'
},
splitLine: {
lineStyle: {
color: 'rgba(255, 255, 255, 0.1)'
}
},
axisLine: {
lineStyle: {
color: 'rgba(255, 255, 255, 0.3)'
}
},
max: isPercentage ? 1 : undefined
},
series: [{
name: isPercentage ? '百分比' : '用量',
type: 'bar',
barWidth: '60%',
data: [
{ value: previousValue, itemStyle: { color: props.previousColor } },
{ value: currentValue, itemStyle: { color: props.currentColor } }
],
label: {
show: true,
position: 'top',
fontWeight: 'bold',
color: '#fff',
formatter: (params: any) => {
if (params.value <= 0) return '';
return isPercentage ? `${(params.value * 100).toFixed(1)}%` : params.value.toFixed(2);
}
}
}],
//
title: props.chartData[0]?.growthRate !== undefined ? {
text: `
数差: ${(currentValue - previousValue).toFixed(2)}${props.chartType === 'percentage' ? '%' : ''}
${props.chartData[0].growthRate >= 0 ? '↑' : '↓'} ${(props.chartData[0].growthRate * 100).toFixed(1)}%`,
left: 'center',
top: '0%',
textStyle: {
fontSize: 36,
fontWeight: 'bold',
color: props.chartData[0].growthRate >= 0 ? '#52c41a' : '#f5222d',
fontFamily: 'Arial, sans-serif'
}
} : undefined
};
}
//
const initChart = () => {
if (chartContainer.value) {
chartInstance = echarts.init(chartContainer.value)
updateChart()
}
}
//
const updateChart = () => {
if (!chartInstance || !props.chartData.length) return
const dataItem = props.chartData[0]
// 使period使
const currentLabel = dataItem.currentPeriod || props.currentLabel
const previousLabel = dataItem.previousPeriod || props.previousLabel
const option = createBarOption(
currentLabel,
previousLabel,
dataItem.currentValue,
dataItem.previousValue
)
chartInstance.setOption(option)
}
//
watch(
() => props.chartData,
() => {
updateChart()
},
{ deep: true }
)
//
const handleResize = () => {
if (chartInstance) {
chartInstance.resize()
}
}
//
onMounted(() => {
initChart()
window.addEventListener('resize', handleResize)
})
//
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize)
if (chartInstance) {
chartInstance.dispose()
chartInstance = null
}
})
</script>
<style scoped>
.comparison-bar-chart-container {
width: 100%;
height: 100%;
}
</style>

52
public/map/components/dictionaryTable.ts

@ -60,6 +60,15 @@ export const DOCK_DISTRICT = async () => {
value: item.id
}))
}
// 泊位map
export const BERTH_DISTRICT = async () => {
const res = await MapApi.getBerthIdAndNameList()
return res.map(item => ({
...item,
label: item.name,
value: item.id
}))
}
// 岸电状态
export const SHORE_POWER_STATUS = [
@ -117,4 +126,45 @@ export const APPLY_STATUS = [
{ label: '用电完成', value: 9 },
{ label: '申请完成', value: 10 },
{ label: '已取消', value: 11 },
]
]
/* 岸电一级状态 */
export const SHORE_POWER_FIRST_STATUS = [
{ label: '故障', value: 1 }, // 检修
{ label: '故障', value: 2 }, // 跳闸
{ label: '在用', value: 3 }, // 合闸 → 正在供电使用
{ label: '正常', value: 4 }, // 分闸 → 空闲但正常
{ label: '故障', value: 5 }, // 通信异常
{ label: '故障', value: 8 }, // 无法解析设备状态
{ label: '正常', value: 9 }, // 未开始采集数据
]
/* 岸电二级状态 */
export const SHORE_POWER_SECOND_STATUS_MAP = [
{ label: '检修', value: 1 },
{ label: '跳闸', value: 2 },
{ label: '合闸', value: 3 },
{ label: '分闸', value: 4 },
{ label: '通信异常', value: 5 },
{ label: '超出最大容量', value: 6 },
{ label: '无法解析设备状态', value: 8 },
{ label: '未开始采集数据', value: 9 },
]
/* 船舶一级状态 */
export const SHIP_FIRST_STATUS = [
{ label: '在用', value: 1 },
{ label: '故障', value: 2 },
{ label: '正常', value: 3 },
{ label: '异常', value: 4 },
]
/* 船舶二级状态 */
export const SHIP_SECOND_STATUS_MAP = [
{ label: '在用', value: 1 },
{ label: '超容', value: 2 },
{ label: '故障', value: 3 },
{ label: '获取岸电容量失败', value: 4 },
{ label: '未使用岸电', value: 5 },
{ label: '跳闸', value: 6 },
]

1067
public/map/index.vue

File diff suppressed because it is too large

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

@ -63,7 +63,6 @@ export const MapApi = {
? `?parentId=${parentId}`
: ''
const data = await request.get({ url: `/shorepower/map/expand/selectAll` + params })
console.log('data:', data)
return data
},
// 查询船舶信息
@ -179,5 +178,12 @@ export const MapApi = {
url: `/shorepower/shore-power-and-ship/expand/getHistoryPage`,
params
})
},
getApplyShipCount: async (): Promise<apiResponse<any>> => {
return await request.get({
url: `shorepower/apply/getShipCount`
// params
})
}
}

96
src/types/shorepower.d.ts

@ -11,6 +11,7 @@ interface ShorePowerBerth {
createTime: number // Unix timestamp in milliseconds
shorePowerEquipmentInfo: ShorePowerEquipmentInfo
storePowerStatus?: string
position?: string
}
interface ShorePowerEquipmentInfo {
@ -397,11 +398,11 @@ export interface UsageRecordRespVO {
/**
*
*/
actualBerthTime?: Date
actualBerthTime?: number
/**
*
*/
actualDepartureTime?: number | null | undefined
actualDepartureTime?: number
/**
*
*/
@ -477,3 +478,94 @@ interface RealtimeDeviceData {
measureTime: number // 时间戳(毫秒)
measureValue: number
}
type TimeRange = 'year' | 'month' | 'day' | 'realtime'
type CompanyShorePowerBuildDataItem = {
[key in TimeRange]: {
name: string
value: number
harborDistrictId: number
totalPower: number
children: {
name: string
value: number
}[]
}
}
interface ComparativeData {
electricityConsumption: MetricData
utilizationRate: MetricData
}
interface MetricData {
day: {
growthRate: number
current: {
period: string | null
value: number
}
previous: {
period: string | null
value: number
}
}
dayYearOnYear: {
growthRate: number
current: {
period: string | null
value: number
}
previous: {
period: string | null
value: number
}
}
month: {
growthRate: number
current: {
period: string | null
value: number
}
previous: {
period: string | null
value: number
}
}
monthYearOnYear: {
growthRate: number
current: {
period: string | null
value: number
}
previous: {
period: string | null
value: number
}
}
year: {
growthRate: number
current: {
period: string | null
value: number
}
previous: {
period: string | null
value: number
}
}
}
export interface CompanyShorePowerData {
id: number
harborDistrictId: number
name?: string
shorePowerCount: number // 岸电箱总数
shorePowerIds: number[] // 岸电箱ID列表
distributionList: {
id: number
name: string
shorePowerList: any[]
}[]
}

Loading…
Cancel
Save