|
|
|
|
<template>
|
|
|
|
|
<div>
|
|
|
|
|
<PublicMapComponents ref="mapComponentRef" class="map-base" :shore-power-list="ShorePowerList"
|
|
|
|
|
:ship-data-list="shipDataList" v-if="dataLoad" :on-instance-click="handleElectricalBoxClick" />
|
|
|
|
|
<div v-else class="loading">
|
|
|
|
|
<div style="margin: 0 auto">
|
|
|
|
|
loading
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="head">
|
|
|
|
|
<div class="head-title">
|
|
|
|
|
<span>曹妃甸港区船舶岸电监管平台</span>
|
|
|
|
|
</div>
|
|
|
|
|
<el-button class="view-btn" size="small" type="success" @click="handleOverviewClick"
|
|
|
|
|
style="margin-right: 10px;">概览视角</el-button>
|
|
|
|
|
|
|
|
|
|
<div class="head-group">
|
|
|
|
|
<div v-for="item in headGroupList" :key="item.value" class="head-group-item"
|
|
|
|
|
:class="{ 'active': activeHeadGroup === item.value }" @click="selectHeadGroup(item.value)">
|
|
|
|
|
{{ item.label }}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="head-time">{{ currentTime }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 港区概览 -->
|
|
|
|
|
<template v-if="activeHeadGroup === 0">
|
|
|
|
|
<PortOverview :ship-status-data="shipStatusData" :shore-power-status-data="shorePowerStatusData"
|
|
|
|
|
@item-click="handleSwitch" :realtime-device-data="realtimeDeviceData" :shore-power-list="ShorePowerList"
|
|
|
|
|
:ship-data-list="shipDataList" />
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<!-- 港口岸电使用情况 -->
|
|
|
|
|
<!-- <template v-if="activeHeadGroup === 1"> -->
|
|
|
|
|
<ShorePowerUsage v-show="activeHeadGroup === 1" :realtime-device-data="realtimeDeviceData"
|
|
|
|
|
:active-head-group="activeHeadGroup" />
|
|
|
|
|
<!-- </template> -->
|
|
|
|
|
|
|
|
|
|
<!-- 港口企业岸电使用 -->
|
|
|
|
|
<template v-if="activeHeadGroup === 2">
|
|
|
|
|
<CompanyShorePower :companyComparisonData="companyComparisonData" :realtime-device-data="realtimeDeviceData" />
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<!-- 船舶岸电使用情况 -->
|
|
|
|
|
<template v-if="activeHeadGroup === 3">
|
|
|
|
|
<ShipShorePower :ship-data="shipDataList" :selected-ship="selectedShip" @item-click="handleSwitch" />
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<template v-if="activeHeadGroup === 4">
|
|
|
|
|
<div v-if="selectedElectricalBox.type" class="right" style="width: 20%">
|
|
|
|
|
<div v-if="selectedElectricalBox.type === 'ship'" class="right-two-row">
|
|
|
|
|
<div class="card digital-twin-card--deep-blue">
|
|
|
|
|
<div class="card-title">
|
|
|
|
|
<el-button class="close-btn" size="small" type="text" @click="closeElectricalBoxInfo">×</el-button>
|
|
|
|
|
<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">{{ selectedElectricalBox.data.shipBasicInfo.name + '-' +
|
|
|
|
|
selectedElectricalBox.data.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">{{ selectedElectricalBox.data.shipBasicInfo.length }} 米</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="detail-item">
|
|
|
|
|
<span class="label">宽度:</span>
|
|
|
|
|
<span class="value">{{ selectedElectricalBox.data.shipBasicInfo.width }} 米</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="detail-item">
|
|
|
|
|
<span class="label">吨位:</span>
|
|
|
|
|
<span class="value">{{ selectedElectricalBox.data.shipBasicInfo.tonnage }} 吨</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="detail-item">
|
|
|
|
|
<span class="label">满载吃水深度:</span>
|
|
|
|
|
<span class="value">{{ selectedElectricalBox.data.shipBasicInfo.fullLoadDraft }} 米</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="detail-item">
|
|
|
|
|
<span class="label">电压:</span>
|
|
|
|
|
<span class="value">{{ getValueById(realtimeDeviceData,
|
|
|
|
|
selectedElectricalBox.data.shorePower.voltageDeviceId,
|
|
|
|
|
'measureValue') }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="detail-item">
|
|
|
|
|
<span class="label">电流:</span>
|
|
|
|
|
<span class="value">{{ getValueById(realtimeDeviceData,
|
|
|
|
|
selectedElectricalBox.data.shorePower.currentDeviceId,
|
|
|
|
|
'measureValue') }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="detail-item">
|
|
|
|
|
<span class="label">频率:</span>
|
|
|
|
|
<span class="value">{{ getValueById(realtimeDeviceData,
|
|
|
|
|
selectedElectricalBox.data.shorePower.frequencyDeviceId,
|
|
|
|
|
'measureValue') }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="detail-item">
|
|
|
|
|
<span class="label">靠泊状态:</span>
|
|
|
|
|
<span class="value">{{ getOperationTypeLabel(selectedElectricalBox.data.shorePowerAndShip.status,
|
|
|
|
|
SHORE_POWER_STATUS,
|
|
|
|
|
) }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="detail-item">
|
|
|
|
|
<span class="label">靠泊类型:</span>
|
|
|
|
|
<span class="value">{{ getOperationTypeLabel(selectedElectricalBox.data.shorePowerAndShip.type,
|
|
|
|
|
BERTH_TYPE)
|
|
|
|
|
}}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="detail-item">
|
|
|
|
|
<span class="label">靠泊时间:</span>
|
|
|
|
|
<span class="value">{{
|
|
|
|
|
formatTimestamp(selectedElectricalBox.data?.usageRecordInfo?.actualBerthTime) }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="detail-item">
|
|
|
|
|
<span class="label">当前状态:</span>
|
|
|
|
|
<span class="value">{{ selectedElectricalBox.data.shipStatus }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-if="selectedElectricalBox.data.applyInfo.reason === 0" class="detail-item">
|
|
|
|
|
<span class="label">岸电使用时长</span>
|
|
|
|
|
<span class="value">{{ showStatus(selectedElectricalBox.data, realtimeDeviceData)?.useTime }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-if="selectedElectricalBox.data.applyInfo.reason === 0" class="detail-item">
|
|
|
|
|
<span class="label">岸电使用用量:</span>
|
|
|
|
|
<span class="value">{{ showStatus(selectedElectricalBox.data, realtimeDeviceData)?.useValue }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-if="selectedElectricalBox.data.applyInfo.reason != 0" class="detail-item">
|
|
|
|
|
<span class="label">未使用岸电原因:</span>
|
|
|
|
|
<span class="value">{{ getOperationTypeLabel(selectedElectricalBox.data?.applyInfo?.reason,
|
|
|
|
|
UNUSED_SHORE_POWER_REASON) }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="detail-item">
|
|
|
|
|
<span class="label">岸电联系人:</span>
|
|
|
|
|
<span class="value">{{ selectedElectricalBox.data?.shipBasicInfo?.shorePowerContact
|
|
|
|
|
}}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="detail-item">
|
|
|
|
|
<span class="label">联系方式:</span>
|
|
|
|
|
<span class="value">{{ selectedElectricalBox.data?.shipBasicInfo?.shorePowerContactPhone
|
|
|
|
|
}}</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="selectedElectricalBox.type === 'shorepower_box'" class="right-two-row">
|
|
|
|
|
<div class="card digital-twin-card--deep-blue">
|
|
|
|
|
<div class="card-title">
|
|
|
|
|
<el-button class="close-btn" size="small" type="text" @click="closeElectricalBoxInfo">×</el-button>
|
|
|
|
|
<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">{{ selectedElectricalBox.data?.name }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="detail-item">
|
|
|
|
|
<span class="label">位置:</span>
|
|
|
|
|
<span class="value">{{ selectedElectricalBox.data?.position }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="detail-item">
|
|
|
|
|
<span class="label">电压:</span>
|
|
|
|
|
<span class="value">{{ selectedElectricalBox.data?.shorePowerEquipmentInfo?.voltage }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="detail-item">
|
|
|
|
|
<span class="label">频率:</span>
|
|
|
|
|
<span class="value">{{ selectedElectricalBox.data?.shorePowerEquipmentInfo?.frequency }} </span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="detail-item">
|
|
|
|
|
<span class="label">容量:</span>
|
|
|
|
|
<span class="value">{{ selectedElectricalBox.data?.shorePowerEquipmentInfo?.capacity }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="detail-item">
|
|
|
|
|
<span class="label">当前电压:</span>
|
|
|
|
|
<span class="value">{{ getValueById(realtimeDeviceData, selectedElectricalBox.data?.voltageDeviceId,
|
|
|
|
|
'measureValue') }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="detail-item">
|
|
|
|
|
<span class="label">当前电流:</span>
|
|
|
|
|
<span class="value">{{ getValueById(realtimeDeviceData, selectedElectricalBox.data?.currentDeviceId,
|
|
|
|
|
'measureValue') }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="detail-item">
|
|
|
|
|
<span class="label">当前频率:</span>
|
|
|
|
|
<span class="value">{{ getValueById(realtimeDeviceData, selectedElectricalBox.data?.frequencyDeviceId,
|
|
|
|
|
'measureValue') }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="detail-item">
|
|
|
|
|
<span class="label">当前总用量:</span>
|
|
|
|
|
<span class="value">{{ getValueById(realtimeDeviceData,
|
|
|
|
|
selectedElectricalBox.data?.totalPowerDeviceId,
|
|
|
|
|
'measureValue') }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="detail-item">
|
|
|
|
|
<span class="label">当前状态:</span>
|
|
|
|
|
<span class="value">{{ selectedElectricalBox.data?.storePowerStatus }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
|
|
|
|
import PublicMapComponents from './components/cesiumMap.vue'
|
|
|
|
|
import PortOverview from './components/PortOverview.vue'
|
|
|
|
|
import ShorePowerUsage from './components/ShorePowerUsage.vue'
|
|
|
|
|
import CompanyShorePower from './components/CompanyShorePower.vue'
|
|
|
|
|
import ShipShorePower from './components/ShipShorePower.vue'
|
|
|
|
|
import dayjs from 'dayjs'
|
|
|
|
|
import { onMounted, onUnmounted, ref, computed, watch } from 'vue'
|
|
|
|
|
import { MapApi } from "@/api/shorepower/map";
|
|
|
|
|
import { RealtimeDeviceData, ShipBasicInfoRespVO, ShipRespVo, ShorePowerBerth } from '@/types/shorepower'
|
|
|
|
|
import { BERTH_TYPE, DOCK_DISTRICT, getOperationTypeLabel, HARBOR_DISTRICT, SHORE_POWER_STATUS, UNUSED_SHORE_POWER_REASON } from './components/dictionaryTable'
|
|
|
|
|
import { formatDuration, formatTimestamp, getValueById, showStatus } from './components/utils'
|
|
|
|
|
defineOptions({ name: 'PublicMap' })
|
|
|
|
|
let getRealtimeIntervalId: NodeJS.Timeout | null = null
|
|
|
|
|
const headGroupList = ref<{ value: number, label: string }[]>([
|
|
|
|
|
{ value: 0, label: '港区概览' },
|
|
|
|
|
{ value: 1, label: '港口岸电使用情况' },
|
|
|
|
|
{ value: 2, label: '港口企业岸电使用' },
|
|
|
|
|
{ value: 3, label: '船舶岸电使用情况' },
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
const activeHeadGroup = ref<number>(-1)
|
|
|
|
|
|
|
|
|
|
// 定义子组件类型
|
|
|
|
|
interface PublicMapComponentsType {
|
|
|
|
|
switchView: (viewName: string) => void
|
|
|
|
|
dataWithModels: any[]
|
|
|
|
|
switchModelView: (data: any) => void
|
|
|
|
|
setElectricalBoxClickCallback: (callback: (data: any) => void) => void
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const mapComponentRef = ref<PublicMapComponentsType | null>(null)
|
|
|
|
|
|
|
|
|
|
const selectHeadGroup = async (value: number) => {
|
|
|
|
|
if (value === activeHeadGroup.value) {
|
|
|
|
|
activeHeadGroup.value = -1;
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
activeHeadGroup.value = value
|
|
|
|
|
if (value === 1) {
|
|
|
|
|
// const deviceStatus = await MapApi.getDeviceStatusByIds(totalPowerDeviceId.value)
|
|
|
|
|
// console.log('deviceStatus', deviceStatus)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 实时时间显示
|
|
|
|
|
const currentTime = ref<string>(dayjs().format('YYYY-MM-DD HH:mm:ss'))
|
|
|
|
|
|
|
|
|
|
// 实时设备采集值
|
|
|
|
|
const realtimeDeviceData = ref<RealtimeDeviceData[]>([])
|
|
|
|
|
const ShorePowerList = ref<(ShorePowerBerth & { position: string; })[]>([])
|
|
|
|
|
const shipDataList = ref<ShipRespVo[]>([])
|
|
|
|
|
const dataLoad = ref<boolean>(false)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 自动滚动相关变量
|
|
|
|
|
const scrollContainerRef = ref<HTMLElement | null>(null)
|
|
|
|
|
let scrollTimer: NodeJS.Timeout | null = null
|
|
|
|
|
let scrollTimeout: NodeJS.Timeout | null = null
|
|
|
|
|
let userScrollHandler: ((event: Event) => void) | null = null
|
|
|
|
|
const scrollSpeed = ref<number>(20) // 滚动速度,越小越快
|
|
|
|
|
|
|
|
|
|
// 更新时间的函数
|
|
|
|
|
const updateTime = () => {
|
|
|
|
|
currentTime.value = dayjs().format('YYYY-MM-DD HH:mm:ss')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const selectedShip = ref(null)
|
|
|
|
|
const selectedElectricalBox = ref({
|
|
|
|
|
type: '',
|
|
|
|
|
data: null
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
/* 回到初始视角 */
|
|
|
|
|
const handleOverviewClick = () => {
|
|
|
|
|
mapComponentRef.value?.switchView('overview')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 切换至模型视角 */
|
|
|
|
|
const handleSwitch = (item) => {
|
|
|
|
|
console.log(item)
|
|
|
|
|
// selectedShip.value = item
|
|
|
|
|
mapComponentRef.value?.switchModelView(item)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* const handleFlyToitem = (item) => {
|
|
|
|
|
mapComponentRef.value?.switchModelView(item)
|
|
|
|
|
} */
|
|
|
|
|
|
|
|
|
|
// 处理岸电箱点击事件
|
|
|
|
|
const handleElectricalBoxClick = (data) => {
|
|
|
|
|
activeHeadGroup.value = 4
|
|
|
|
|
console.log('岸电箱被点击:', data);
|
|
|
|
|
// 设置选中的岸电箱数据
|
|
|
|
|
// selectedElectricalBox.value = data;
|
|
|
|
|
selectedElectricalBox.value = {
|
|
|
|
|
type: data.type,
|
|
|
|
|
data: data.data
|
|
|
|
|
}
|
|
|
|
|
// 这里可以添加更多的处理逻辑,比如切换到特定的视图等
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 关闭岸电箱详情面板
|
|
|
|
|
const closeElectricalBoxInfo = () => {
|
|
|
|
|
activeHeadGroup.value = -1;
|
|
|
|
|
selectedElectricalBox.value = {
|
|
|
|
|
type: '',
|
|
|
|
|
data: null
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 启动定时器更新时间
|
|
|
|
|
let timer: NodeJS.Timeout
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
timer = setInterval(updateTime, 1000)
|
|
|
|
|
|
|
|
|
|
// 设置岸电箱点击回调
|
|
|
|
|
if (mapComponentRef.value) {
|
|
|
|
|
console.log('mapComponentRef.value', mapComponentRef.value)
|
|
|
|
|
mapComponentRef.value.setElectricalBoxClickCallback(handleElectricalBoxClick);
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 组件卸载时清除所有资源
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
console.log('Unmounting component, cleaning up resources')
|
|
|
|
|
|
|
|
|
|
// 清除时间更新定时器
|
|
|
|
|
if (timer) {
|
|
|
|
|
clearInterval(timer)
|
|
|
|
|
timer = null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 清除自动滚动定时器
|
|
|
|
|
if (scrollTimer) {
|
|
|
|
|
clearInterval(scrollTimer)
|
|
|
|
|
scrollTimer = null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 清除用户滚动超时
|
|
|
|
|
if (scrollTimeout) {
|
|
|
|
|
clearTimeout(scrollTimeout)
|
|
|
|
|
scrollTimeout = null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 移除用户滚动事件监听器
|
|
|
|
|
if (scrollContainerRef.value && userScrollHandler) {
|
|
|
|
|
scrollContainerRef.value.removeEventListener('scroll', userScrollHandler)
|
|
|
|
|
userScrollHandler = null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log('All resources cleaned up')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 自动滚动功能
|
|
|
|
|
const startAutoScroll = () => {
|
|
|
|
|
if (!scrollContainerRef.value) {
|
|
|
|
|
console.log('scrollContainerRef is null')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const container = scrollContainerRef.value
|
|
|
|
|
const content = container.querySelector('.ship-data-table')
|
|
|
|
|
if (!content) {
|
|
|
|
|
console.log('ship-data-table not found')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const contentHeight = content.offsetHeight
|
|
|
|
|
const containerHeight = container.offsetHeight
|
|
|
|
|
|
|
|
|
|
console.log('Container height:', containerHeight)
|
|
|
|
|
console.log('Content height:', contentHeight)
|
|
|
|
|
|
|
|
|
|
// 如果内容高度小于等于容器高度,不需要滚动
|
|
|
|
|
if (contentHeight <= containerHeight) {
|
|
|
|
|
console.log('Content height is less than container height, no need to scroll')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 清除之前的定时器
|
|
|
|
|
if (scrollTimer) {
|
|
|
|
|
clearInterval(scrollTimer)
|
|
|
|
|
scrollTimer = null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (scrollTimeout) {
|
|
|
|
|
clearTimeout(scrollTimeout)
|
|
|
|
|
scrollTimeout = null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 移除之前的事件监听器
|
|
|
|
|
if (userScrollHandler) {
|
|
|
|
|
container.removeEventListener('scroll', userScrollHandler)
|
|
|
|
|
userScrollHandler = null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 监听滚动事件,当用户手动滚动时暂停自动滚动
|
|
|
|
|
let isUserScrolling = false
|
|
|
|
|
|
|
|
|
|
userScrollHandler = (event) => {
|
|
|
|
|
console.log('User scrolling detected')
|
|
|
|
|
isUserScrolling = true
|
|
|
|
|
|
|
|
|
|
// 清除自动滚动定时器
|
|
|
|
|
if (scrollTimer) {
|
|
|
|
|
clearInterval(scrollTimer)
|
|
|
|
|
scrollTimer = null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 清除之前的超时
|
|
|
|
|
if (scrollTimeout) {
|
|
|
|
|
clearTimeout(scrollTimeout)
|
|
|
|
|
scrollTimeout = null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 3秒后恢复自动滚动
|
|
|
|
|
scrollTimeout = setTimeout(() => {
|
|
|
|
|
console.log('Resuming auto scroll after user interaction')
|
|
|
|
|
isUserScrolling = false
|
|
|
|
|
startAutoScroll()
|
|
|
|
|
}, 3000)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 添加事件监听器
|
|
|
|
|
container.addEventListener('scroll', userScrollHandler)
|
|
|
|
|
|
|
|
|
|
// 启动自动滚动
|
|
|
|
|
scrollTimer = setInterval(() => {
|
|
|
|
|
if (isUserScrolling) return
|
|
|
|
|
|
|
|
|
|
// 计算当前滚动位置
|
|
|
|
|
const currentScrollTop = container.scrollTop
|
|
|
|
|
const maxScrollTop = contentHeight - containerHeight
|
|
|
|
|
|
|
|
|
|
// 检查内容是否有变化
|
|
|
|
|
if (content.offsetHeight !== contentHeight) {
|
|
|
|
|
console.log('Content height changed, restarting scroll')
|
|
|
|
|
startAutoScroll()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log('Auto scrolling - Current scroll:', currentScrollTop, 'Max scroll:', maxScrollTop)
|
|
|
|
|
|
|
|
|
|
if (currentScrollTop >= maxScrollTop) {
|
|
|
|
|
// 滚动到顶部
|
|
|
|
|
console.log('Reached bottom, scrolling to top')
|
|
|
|
|
container.scrollTop = 0
|
|
|
|
|
} else {
|
|
|
|
|
// 平滑滚动
|
|
|
|
|
container.scrollTop += 1
|
|
|
|
|
}
|
|
|
|
|
}, scrollSpeed.value)
|
|
|
|
|
|
|
|
|
|
console.log('Auto scroll started')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleGetRealtimeAllData = async () => {
|
|
|
|
|
const data = await MapApi.getRealtimeAllData({})
|
|
|
|
|
const arrayOfObjects = Object.values(data) as RealtimeDeviceData[]
|
|
|
|
|
realtimeDeviceData.value = arrayOfObjects
|
|
|
|
|
// console.log('getRealtimeAllData', arrayOfObjects)
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const StatusMap = {
|
|
|
|
|
1: '故障',
|
|
|
|
|
2: '故障',
|
|
|
|
|
3: '在线',
|
|
|
|
|
4: '在线',
|
|
|
|
|
5: '离线',
|
|
|
|
|
6: '在线',
|
|
|
|
|
8: '故障',
|
|
|
|
|
9: '离线',
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 状态转换函数
|
|
|
|
|
const StatusText = (status: number | string) => {
|
|
|
|
|
const statusNum = Number(status);
|
|
|
|
|
// console.log(status)
|
|
|
|
|
return StatusMap[statusNum] || status;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleGetStorePower = async () => {
|
|
|
|
|
// console.log('loading')
|
|
|
|
|
const harborDistrictId = 1
|
|
|
|
|
const harborDistrictList = await HARBOR_DISTRICT()
|
|
|
|
|
const dockDistrictList = await DOCK_DISTRICT()
|
|
|
|
|
const res = await MapApi.getShorepowerIdAndNameListByHarborDistrictId(harborDistrictId)
|
|
|
|
|
ShorePowerList.value = res.map(item => ({
|
|
|
|
|
...item,
|
|
|
|
|
storePowerStatus: StatusText(item.status),
|
|
|
|
|
position: `${getOperationTypeLabel(harborDistrictId, harborDistrictList)}${getOperationTypeLabel(item.shorePowerEquipmentInfo.dockId, dockDistrictList)}${item.name} `,
|
|
|
|
|
}))
|
|
|
|
|
// console.log('11', res)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const getShipStatus = (ship: ShipRespVo) => {
|
|
|
|
|
// console.log(props.realtimeDeviceData)
|
|
|
|
|
if (ship.applyInfo.reason !== 0) {
|
|
|
|
|
return '未使用'
|
|
|
|
|
} else if ([2, 5, 8].includes(ship.shorePowerAndShip.status)) {
|
|
|
|
|
return '故障'
|
|
|
|
|
} else {
|
|
|
|
|
const Vobj = realtimeDeviceData.value.find(item => item.deviceId === ship.shorePower.voltageDeviceld)
|
|
|
|
|
const Cobj = realtimeDeviceData.value.find(item => item.deviceId === ship.shorePower.currentDeviceld)
|
|
|
|
|
let capacity = 0
|
|
|
|
|
try {
|
|
|
|
|
capacity = Number(ship.shorePowerEquipment.capacity)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return '获取岸电容量失败'
|
|
|
|
|
}
|
|
|
|
|
const power = (Vobj?.measureValue ?? 0) * (Cobj?.measureValue ?? 0);
|
|
|
|
|
if (power > capacity) {
|
|
|
|
|
return '超容'
|
|
|
|
|
}
|
|
|
|
|
return '正常'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleGetShipData = async () => {
|
|
|
|
|
const shipData = await MapApi.getShipInfo({
|
|
|
|
|
harborDistrictId: 1
|
|
|
|
|
})
|
|
|
|
|
const buildData = shipData.map(item => ({
|
|
|
|
|
...item,
|
|
|
|
|
shipStatus: getShipStatus(item),
|
|
|
|
|
}))
|
|
|
|
|
shipDataList.value = buildData
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 监听组件挂载
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
// 延迟启动滚动,确保DOM已渲染
|
|
|
|
|
await handleGetRealtimeAllData()
|
|
|
|
|
await handleGetStorePower()
|
|
|
|
|
await handleGetShipData()
|
|
|
|
|
dataLoad.value = true
|
|
|
|
|
getRealtimeIntervalId = setInterval(() => {
|
|
|
|
|
handleGetRealtimeAllData()
|
|
|
|
|
}, 5000)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 监听组件卸载
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
if (getRealtimeIntervalId) {
|
|
|
|
|
clearInterval(getRealtimeIntervalId)
|
|
|
|
|
getRealtimeIntervalId = null
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 监听组件挂载
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
timer = setInterval(updateTime, 1000)
|
|
|
|
|
// 延迟启动滚动,确保DOM已渲染
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
startAutoScroll()
|
|
|
|
|
}, 500)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 监听活跃头部组变化,当切换到第3组时重新启动滚动
|
|
|
|
|
watch(
|
|
|
|
|
() => activeHeadGroup,
|
|
|
|
|
(newValue) => {
|
|
|
|
|
if (newValue === 3) {
|
|
|
|
|
// 延迟启动滚动,确保DOM已重新渲染
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
startAutoScroll()
|
|
|
|
|
}, 300)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// 监听子组件数据变化
|
|
|
|
|
watch(
|
|
|
|
|
() => mapComponentRef.value?.dataWithModels,
|
|
|
|
|
(newData) => {
|
|
|
|
|
console.log('子组件数据变化:', newData)
|
|
|
|
|
},
|
|
|
|
|
{ deep: true }
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// 三个公司的岸电使用对比数据
|
|
|
|
|
const companyComparisonData = ref([
|
|
|
|
|
{
|
|
|
|
|
name: '华能码头',
|
|
|
|
|
value: 234277.33,
|
|
|
|
|
children: [
|
|
|
|
|
{ name: '1泊位', value: 80000 },
|
|
|
|
|
{ name: '2泊位', value: 60000 },
|
|
|
|
|
{ name: '3泊位', value: 40000 },
|
|
|
|
|
{ name: '4泊位', value: 30000 },
|
|
|
|
|
{ name: '5泊位', value: 24277.33 }
|
|
|
|
|
]
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: '国投码头',
|
|
|
|
|
value: 417203.39,
|
|
|
|
|
children: [
|
|
|
|
|
{ name: '101泊位', value: 150000 },
|
|
|
|
|
{ name: '102泊位', value: 100000 },
|
|
|
|
|
{ name: '103泊位', value: 80000 },
|
|
|
|
|
{ name: '104泊位', value: 50000 },
|
|
|
|
|
{ name: '105泊位', value: 37203.39 },
|
|
|
|
|
{ name: '106泊位', value: 5433 },
|
|
|
|
|
{ name: '107泊位', value: 34567 },
|
|
|
|
|
{ name: '108泊位', value: 50000 },
|
|
|
|
|
{ name: '109泊位', value: 50000 },
|
|
|
|
|
{ name: '110泊位', value: 50000 },
|
|
|
|
|
|
|
|
|
|
]
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: '华电码头(储运)',
|
|
|
|
|
value: 191340.14,
|
|
|
|
|
children: [
|
|
|
|
|
{ name: '11泊位', value: 80000 },
|
|
|
|
|
{ name: '12泊位', value: 60000 },
|
|
|
|
|
{ name: '13泊位', value: 40000 },
|
|
|
|
|
{ name: '14泊位', value: 30000 },
|
|
|
|
|
{ name: '15泊位', value: 24277.33 }
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
// 下拉框选中的公司
|
|
|
|
|
const selectedCompany = ref('华能码头')
|
|
|
|
|
|
|
|
|
|
// 饼图数据
|
|
|
|
|
const pieChartData = ref<Array<{ name: string; value: number }>>([])
|
|
|
|
|
|
|
|
|
|
// 初始化饼图数据
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
// 默认选中华能码头的数据
|
|
|
|
|
const defaultCompany = companyComparisonData.value.find(company => company.name === selectedCompany.value)
|
|
|
|
|
if (defaultCompany && defaultCompany.children) {
|
|
|
|
|
pieChartData.value = defaultCompany.children
|
|
|
|
|
}
|
|
|
|
|
/* console.log('totalPowerDeviceId', totalPowerDeviceId.value)
|
|
|
|
|
const deviceStatus = await MapApi.getDeviceStatusByIds(totalPowerDeviceId.value)
|
|
|
|
|
console.log('deviceStatus', deviceStatus) */
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const totalPowerDeviceId = computed(() => {
|
|
|
|
|
if (!mapComponentRef.value || !mapComponentRef.value.dataWithModels) {
|
|
|
|
|
return []
|
|
|
|
|
}
|
|
|
|
|
const ids = mapComponentRef.value.dataWithModels
|
|
|
|
|
.filter(item => item.modelType === 'ship')
|
|
|
|
|
.map(item => item.shorePower?.totalPowerDeviceId)
|
|
|
|
|
.filter(id => id !== undefined && id !== null)
|
|
|
|
|
// 去重处理
|
|
|
|
|
return [...new Set(ids)]
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 从子组件获取的岸电箱状态数据
|
|
|
|
|
const shorePowerStatusData = computed(() => {
|
|
|
|
|
if (!mapComponentRef.value || !mapComponentRef.value.dataWithModels) {
|
|
|
|
|
return []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 过滤出轮船类的数据
|
|
|
|
|
return mapComponentRef.value.dataWithModels
|
|
|
|
|
.filter(item => item.modelType === 'electrical_box')
|
|
|
|
|
.map((item, index) => {
|
|
|
|
|
// 解析数据中的状态信息
|
|
|
|
|
let status = ['正常'] // 默认状态
|
|
|
|
|
try {
|
|
|
|
|
if (item.data && typeof item.data === 'string') {
|
|
|
|
|
const dataObj = JSON.parse(item.data)
|
|
|
|
|
// 根据实际数据结构提取状态信息
|
|
|
|
|
// 这里假设dataObj中有status字段,如果没有则使用默认值
|
|
|
|
|
if (dataObj.status) {
|
|
|
|
|
status = Array.isArray(dataObj.status) ? dataObj.status : [dataObj.status]
|
|
|
|
|
}
|
|
|
|
|
} else if (item.data && item.data.status) {
|
|
|
|
|
status = Array.isArray(item.data.status) ? item.data.status : [item.data.status]
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('解析船舶状态失败:', error)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
id: item.id || index + 1,
|
|
|
|
|
...item
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 从子组件获取的船舶数据
|
|
|
|
|
const shipStatusData = computed(() => {
|
|
|
|
|
if (!mapComponentRef.value || !mapComponentRef.value.dataWithModels) {
|
|
|
|
|
return []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 过滤出轮船类的数据
|
|
|
|
|
return mapComponentRef.value.dataWithModels
|
|
|
|
|
.filter(item => item.modelType === 'ship')
|
|
|
|
|
.map((item, index) => {
|
|
|
|
|
// 解析数据中的状态信息
|
|
|
|
|
let status = ['正常'] // 默认状态
|
|
|
|
|
try {
|
|
|
|
|
if (item.data && typeof item.data === 'string') {
|
|
|
|
|
const dataObj = JSON.parse(item.data)
|
|
|
|
|
// 根据实际数据结构提取状态信息
|
|
|
|
|
// 这里假设dataObj中有status字段,如果没有则使用默认值
|
|
|
|
|
if (dataObj.status) {
|
|
|
|
|
status = Array.isArray(dataObj.status) ? dataObj.status : [dataObj.status]
|
|
|
|
|
}
|
|
|
|
|
} else if (item.data && item.data.status) {
|
|
|
|
|
status = Array.isArray(item.data.status) ? item.data.status : [item.data.status]
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('解析船舶状态失败:', error)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
id: item.id || index + 1,
|
|
|
|
|
...item
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
</script>
|
|
|
|
|
<style lang="scss">
|
|
|
|
|
.cesium-viewer-bottom {
|
|
|
|
|
display: none !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.digital-twin-card--deep-blue {
|
|
|
|
|
/* 深空蓝背景 */
|
|
|
|
|
background: rgba(8, 15, 30, 0.7);
|
|
|
|
|
/* 接近纯黑但带蓝调 */
|
|
|
|
|
backdrop-filter: blur(10px);
|
|
|
|
|
-webkit-backdrop-filter: blur(10px);
|
|
|
|
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
border-radius: 2px;
|
|
|
|
|
position: relative;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
|
|
|
|
/* 外边框:霓虹蓝 */
|
|
|
|
|
border: 1px solid rgba(30, 120, 255, 0.4);
|
|
|
|
|
box-shadow:
|
|
|
|
|
0 4px 20px rgba(0, 0, 0, 0.5),
|
|
|
|
|
inset 0 0 0 1px rgba(30, 120, 255, 0.1);
|
|
|
|
|
|
|
|
|
|
/* 文字颜色:冷白/青白 */
|
|
|
|
|
color: #e6f2ff;
|
|
|
|
|
font-family: 'Segoe UI', 'Helvetica Neue', 'IBM Plex Mono', monospace;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 内发光效果 */
|
|
|
|
|
.digital-twin-card--deep-blue::before {
|
|
|
|
|
content: '';
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 0;
|
|
|
|
|
left: 0;
|
|
|
|
|
right: 0;
|
|
|
|
|
bottom: 0;
|
|
|
|
|
border-radius: 16px;
|
|
|
|
|
box-shadow: inset 0 0 16px rgba(30, 120, 255, 0.25);
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 悬停:增强光效 + 轻微上浮 */
|
|
|
|
|
.digital-twin-card--deep-blue:hover {
|
|
|
|
|
border-color: rgba(64, 158, 255, 0.8);
|
|
|
|
|
background: rgba(10, 20, 40, 0.85);
|
|
|
|
|
transition: all 0.35s cubic-bezier(0.25, 0.8, 0.25, 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 可选:顶部高光条(增强“面板”感) */
|
|
|
|
|
.digital-twin-card--deep-blue::after {
|
|
|
|
|
content: '';
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 0;
|
|
|
|
|
left: 0;
|
|
|
|
|
right: 0;
|
|
|
|
|
height: 1px;
|
|
|
|
|
background: linear-gradient(90deg, #00b0ff, #0066ff);
|
|
|
|
|
opacity: 0.7;
|
|
|
|
|
border-radius: 16px 16px 0 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.card {
|
|
|
|
|
// background-color: rgba(10, 130, 170, 0.1);
|
|
|
|
|
// padding: 12px;
|
|
|
|
|
// border-radius: 4px;
|
|
|
|
|
// border: 1px solid rgb(10, 130, 170);
|
|
|
|
|
flex: 1;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
|
|
|
|
.search-container {
|
|
|
|
|
height: 100%;
|
|
|
|
|
width: 200px;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
border: 1px solid rgb(10, 130, 170);
|
|
|
|
|
padding: 0 10px;
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
background-color: rgba(255, 255, 255, 0.1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.card-title {
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: #FFF;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
position: relative;
|
|
|
|
|
|
|
|
|
|
.close-btn {
|
|
|
|
|
position: absolute;
|
|
|
|
|
right: 0;
|
|
|
|
|
top: 50%;
|
|
|
|
|
transform: translateY(-50%);
|
|
|
|
|
color: #FFF;
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
padding: 0;
|
|
|
|
|
width: 30px;
|
|
|
|
|
height: 30px;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
color: #ff4d4f;
|
|
|
|
|
background-color: rgba(255, 77, 79, 0.1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.vertical-line {
|
|
|
|
|
width: 2px;
|
|
|
|
|
height: 20px;
|
|
|
|
|
background-color: #1296db;
|
|
|
|
|
margin-right: 6px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.title-icon {
|
|
|
|
|
width: 20px;
|
|
|
|
|
height: 20px;
|
|
|
|
|
margin-right: 6px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.title-text {
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
color: #FFF;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.card-content {
|
|
|
|
|
flex: 1;
|
|
|
|
|
min-height: 0;
|
|
|
|
|
width: 100%;
|
|
|
|
|
// padding: 10px;
|
|
|
|
|
height: 100%;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 船舶数据表格样式 */
|
|
|
|
|
.ship-data-table-container .card-content {
|
|
|
|
|
height: 100%;
|
|
|
|
|
padding: 10px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.ship-data-table-container {
|
|
|
|
|
height: 100%;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
position: relative;
|
|
|
|
|
scroll-behavior: smooth;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 自定义滚动条样式 */
|
|
|
|
|
.ship-data-table-container::-webkit-scrollbar {
|
|
|
|
|
width: 6px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.ship-data-table-container::-webkit-scrollbar-track {
|
|
|
|
|
background: rgba(255, 255, 255, 0.1);
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.ship-data-table-container::-webkit-scrollbar-thumb {
|
|
|
|
|
background: rgba(17, 138, 237, 0.7);
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.ship-data-table-container::-webkit-scrollbar-thumb:hover {
|
|
|
|
|
background: rgba(17, 138, 237, 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.ship-data-table {
|
|
|
|
|
width: 100%;
|
|
|
|
|
padding: 5px 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.ship-data-row {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
|
|
|
transition: background-color 0.3s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.ship-data-row:hover {
|
|
|
|
|
background-color: rgba(17, 138, 237, 0.2);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.ship-data-cell {
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
color: #fff;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.right-two-row {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 10px;
|
|
|
|
|
height: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.ship-name {
|
|
|
|
|
width: 20%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.wharf-name {
|
|
|
|
|
width: 25%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.berth-number {
|
|
|
|
|
width: 20%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.shore-power-status {
|
|
|
|
|
width: 35%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.head {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
position: absolute;
|
|
|
|
|
width: 100vw;
|
|
|
|
|
height: 72px;
|
|
|
|
|
top: 0px;
|
|
|
|
|
right: 0px;
|
|
|
|
|
|
|
|
|
|
padding: 12px 24px;
|
|
|
|
|
|
|
|
|
|
.head-title {
|
|
|
|
|
width: 420px;
|
|
|
|
|
font-size: 28px;
|
|
|
|
|
color: rgba(20, 200, 255, 1);
|
|
|
|
|
text-align: center;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
text-align: left;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.head-group {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
|
|
|
|
.head-group-item {
|
|
|
|
|
padding: 8px 16px;
|
|
|
|
|
background-color: rgba(50, 50, 50, 0.5);
|
|
|
|
|
color: #ccc;
|
|
|
|
|
border: 1px solid rgb(122, 122, 122);
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.head-group-item.active,
|
|
|
|
|
.head-group-item:hover {
|
|
|
|
|
color: #FFF;
|
|
|
|
|
background-color: rgba(17, 138, 237, 0.4);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.head-time {
|
|
|
|
|
width: 420px;
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
color: #FFF;
|
|
|
|
|
text-align: right;
|
|
|
|
|
text-shadow: 0 0 5px rgba(0, 0, 0, 0.5);
|
|
|
|
|
margin-top: 10px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.left {
|
|
|
|
|
width: 420px;
|
|
|
|
|
padding: 12px;
|
|
|
|
|
height: calc(100vh - 72px);
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 72px;
|
|
|
|
|
left: 0px;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.right {
|
|
|
|
|
width: 420px;
|
|
|
|
|
padding: 12px;
|
|
|
|
|
height: calc(100vh - 72px);
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 72px;
|
|
|
|
|
right: 0px;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 船舶状态表格样式 */
|
|
|
|
|
.ship-table {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
background-color: rgba(0, 0, 0, 0.2);
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 船舶详情样式 */
|
|
|
|
|
.ship-detail {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 1px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.detail-item {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
padding: 4px 0;
|
|
|
|
|
border-bottom: 1px solid rgba(30, 120, 255, 0.2);
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.detail-item:last-child {
|
|
|
|
|
border-bottom: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.detail-item .label {
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
color: #ccc;
|
|
|
|
|
flex: 0 0 120px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.detail-item .value {
|
|
|
|
|
flex: 1;
|
|
|
|
|
text-align: right;
|
|
|
|
|
color: #e6f2ff;
|
|
|
|
|
word-break: break-all;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.no-selection {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
height: 100%;
|
|
|
|
|
color: #ccc;
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.ship-table-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
padding: 10px 15px;
|
|
|
|
|
background-color: rgba(17, 138, 237, 0.3);
|
|
|
|
|
border-bottom: 1px solid rgba(30, 120, 255, 0.4);
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.ship-table-body {
|
|
|
|
|
flex: 1;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
padding: 5px 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.ship-table-row {
|
|
|
|
|
display: flex;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
padding: 8px 8px;
|
|
|
|
|
border-bottom: 1px solid rgba(30, 120, 255, 0.2);
|
|
|
|
|
transition: background-color 0.2s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.ship-table-row:hover {
|
|
|
|
|
background-color: rgba(30, 120, 255, 0.1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.ship-table-column {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.ship-name-header {
|
|
|
|
|
width: 180px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.ship-status-header {
|
|
|
|
|
flex: 1;
|
|
|
|
|
justify-content: flex-start;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.ship-name {
|
|
|
|
|
width: 180px;
|
|
|
|
|
gap: 10px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.ship-icon {
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.ship-name-text {
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
color: #e6f2ff;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.ship-status {
|
|
|
|
|
flex: 1;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
justify-content: flex-start;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-tag {
|
|
|
|
|
padding: 2px 4px;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
color: #fff;
|
|
|
|
|
text-shadow: 0 0 3px rgba(0, 0, 0, 0.5);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-normal {
|
|
|
|
|
background-color: #4CAF50;
|
|
|
|
|
box-shadow: 0 0 5px rgba(76, 175, 80, 0.5);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 空闲状态 */
|
|
|
|
|
.status-idle {
|
|
|
|
|
background-color: #2196F3;
|
|
|
|
|
/* 蓝色 */
|
|
|
|
|
box-shadow: 0 0 5px rgba(33, 150, 243, 0.5);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 故障状态 */
|
|
|
|
|
.status-fault {
|
|
|
|
|
background-color: #F44336;
|
|
|
|
|
/* 红色 */
|
|
|
|
|
box-shadow: 0 0 5px rgba(244, 67, 54, 0.5);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-abnormal {
|
|
|
|
|
background-color: #F44336;
|
|
|
|
|
box-shadow: 0 0 5px rgba(244, 67, 54, 0.5);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-maintenance {
|
|
|
|
|
background-color: #FF9800;
|
|
|
|
|
box-shadow: 0 0 5px rgba(255, 152, 0, 0.5);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-shorepower {
|
|
|
|
|
background-color: #2196F3;
|
|
|
|
|
box-shadow: 0 0 5px rgba(33, 150, 243, 0.5);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* .status-fault {
|
|
|
|
|
background-color: #9C27B0;
|
|
|
|
|
box-shadow: 0 0 5px rgba(156, 39, 176, 0.5);
|
|
|
|
|
} */
|
|
|
|
|
|
|
|
|
|
.status-default {
|
|
|
|
|
background-color: #607D8B;
|
|
|
|
|
box-shadow: 0 0 5px rgba(96, 125, 139, 0.5);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 滚动条样式 */
|
|
|
|
|
.ship-table-body::-webkit-scrollbar,
|
|
|
|
|
.card-content::-webkit-scrollbar {
|
|
|
|
|
width: 6px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.ship-table-body::-webkit-scrollbar-track,
|
|
|
|
|
.card-content::-webkit-scrollbar-track {
|
|
|
|
|
background: rgba(0, 0, 0, 0.2);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.ship-table-body::-webkit-scrollbar-thumb,
|
|
|
|
|
.card-content::-webkit-scrollbar-thumb {
|
|
|
|
|
background: rgba(30, 120, 255, 0.5);
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.ship-table-body::-webkit-scrollbar-thumb:hover,
|
|
|
|
|
.card-content::-webkit-scrollbar-thumb:hover {
|
|
|
|
|
background: rgba(30, 120, 255, 0.7);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 淡入淡出过渡效果 */
|
|
|
|
|
.fade-left-enter-active,
|
|
|
|
|
.fade-left-leave-active,
|
|
|
|
|
.fade-right-enter-active,
|
|
|
|
|
.fade-right-leave-active {
|
|
|
|
|
transition: opacity 0.5s ease, transform 0.5s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.fade-left-enter-from,
|
|
|
|
|
.fade-left-leave-to {
|
|
|
|
|
opacity: 0;
|
|
|
|
|
transform: translateX(-20px);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.fade-right-enter-from,
|
|
|
|
|
.fade-right-leave-to {
|
|
|
|
|
opacity: 0;
|
|
|
|
|
transform: translateX(20px);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.overview-grid {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: 1fr 1fr;
|
|
|
|
|
gap: 2px;
|
|
|
|
|
padding: 2px;
|
|
|
|
|
height: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.overview-item {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
// padding: 4px;
|
|
|
|
|
background-color: rgba(0, 0, 0, 0.2);
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.overview-value {
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
color: #1296db;
|
|
|
|
|
text-shadow: 0 0 5px rgba(0, 0, 0, 0.5);
|
|
|
|
|
margin-bottom: 0px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.overview-label {
|
|
|
|
|
text-align: center;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
color: #ccc;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 状态样式 */
|
|
|
|
|
.status-using {
|
|
|
|
|
color: #00ff00;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-no-equipment {
|
|
|
|
|
color: #ff6600;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-damaged {
|
|
|
|
|
color: #ff3300;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.status-cable {
|
|
|
|
|
color: #ffcc00;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 岸电箱详情样式 */
|
|
|
|
|
.section-title {
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
color: #1296db;
|
|
|
|
|
margin: 15px 0 8px 0;
|
|
|
|
|
padding-bottom: 5px;
|
|
|
|
|
border-bottom: 1px solid rgba(18, 150, 219, 0.3);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.detail-item {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
background-color: rgba(0, 0, 0, 0.15);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.label {
|
|
|
|
|
color: #ddd;
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.value {
|
|
|
|
|
color: #fff;
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
}
|
|
|
|
|
</style>
|