You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

1829 lines
56 KiB

<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 class="loading-container">
<el-space direction="vertical" size="large">
<el-icon :size="60" class="is-loading">
<Loading />
</el-icon>
<!-- <span style="color: white; font-size: 18px;">加载中...</span> -->
</el-space>
</div>
</div>
<div class="head">
<div class="head-group">
<div class="head-group-item" size="small" type="success" @click="handleOverviewClick"
style="margin-right: 10px;">
概览视角</div>
</div>
<div class="head-title">
<span @click="handleClickTitle">曹妃甸港区船舶岸电监管平台</span>
</div>
</div>
<!-- 港区概览 -->
<!-- <template v-if="activeHeadGroup === 0"> -->
<!-- <PortOverview v-show="activeHeadGroup === 0" :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> -->
<ShowData v-show="activeHeadGroup === 0" :handleGoToModule="handleGoToModule"
:realtime-device-data-time="realtimeDeviceDataTime" :chart-data="chartData"
:company-shore-power-build-data="companyShorePowerBuildData" :ship-total-data="shipTotalData"
:ship-data-list="shipDataList" @item-click="handleSwitch" :realtime-device-data="realtimeDeviceData"
v-if="dataLoad" />
<!-- 港口岸电使用情况多数据 -->
<ShorePowerUsage v-show="activeHeadGroup === 1" :realtime-device-data-time="realtimeDeviceDataTime"
:realtime-device-data="realtimeDeviceData" :active-head-group="activeHeadGroup" v-if="dataLoad"
:handleGoBack="handleGoBack" />
<!-- 港口岸电使用情况单数据 -->
<ShorePowerUsageSingleData v-if="activeHeadGroup === 5 && dataLoad"
:realtime-device-data-time="realtimeDeviceDataTime" :realtime-device-data="realtimeDeviceData"
:active-head-group="activeHeadGroup" :handleGoBack="handleGoBack" :comparative-data="comparativeData" />
<!-- 港口企业岸电使用 -->
<!-- <template v-if="activeHeadGroup === 2"> -->
<CompanyShorePower v-show="activeHeadGroup === 2" :realtime-device-data="realtimeDeviceData"
:shore-power-list="ShorePowerList" :handleGoBack="handleGoBack" :year-data="yearDataRes"
:month-data="monthDataRes" :day-data="dayDataRes" :realtime-device-data-time="realtimeDeviceDataTime"
v-if="dataLoad" />
<!-- </template> -->
<!-- 船舶岸电使用情况 -->
<!-- <template > -->
<ShipShorePower v-show="activeHeadGroup === 3" :ship-total-data="shipTotalData" :ship-data="shipDataList"
:selected-ship="selectedShip" @item-click="handleSwitch" :realtime-device-data="realtimeDeviceData"
v-if="dataLoad" :handleGoBack="handleGoBack" :realtime-device-data-time="realtimeDeviceDataTime" />
<!-- </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
v-if="selectedElectricalBox.data.StatusReason">({{ selectedElectricalBox.data.StatusReason
}})</span></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 ShorePowerUsageSingleData from './components/ShorePowerUsageSingleData.vue'
import ShowData from './components/ShowData.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 { Loading } from '@element-plus/icons-vue'
import { MapApi } from "@/api/shorepower/map";
import { CompanyShorePowerBuildDataItem, ComparativeData, 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
interface ChartDataItem {
name: string;
value: number;
}
const headGroupList = ref<{ value: number, label: string }[]>([
{ value: 0, label: '总体概览' },
{ value: 1, label: '港口岸电使用情况' },
{ value: 2, label: '港口企业岸电使用' },
{ value: 3, label: '船舶岸电使用情况' },
])
const activeHeadGroup = ref<number>(0)
// 定义子组件类型
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 realtimeDeviceDataTime = ref<string>('')
const ShorePowerList = ref<(ShorePowerBerth & { position: string; })[]>([])
const shipDataList = ref<ShipRespVo[]>([])
const dataLoad = ref<boolean>(false)
const dockList = ref<{ id: number, name: string }[]>([])
const berthList = ref<{ id: number, name: string, dockId: number }[]>([])
const yearDataRes = ref<any>({})
const monthDataRes = ref<any>({})
const dayDataRes = ref<any>({})
const comparativeData = ref<ComparativeData>({})
// 自动滚动相关变量
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 ChartData = ref<ChartDataItem[]>({
}) */
/* const totalPowerChartData = ref<ChartDataItem[]>([])
const co2ReductionChartData = ref<ChartDataItem[]>([])
const so2ReductionChartData = ref<ChartDataItem[]>([])
const noxReductionChartData = ref<ChartDataItem[]>([])
const pm25ReductionChartData = ref<ChartDataItem[]>([])
const fuelReductionChartData = ref<ChartDataItem[]>([])
*/
const chartData = ref({
'realtime': {
'totalPower': 0,
'fuel': 0,
'co2': 0,
'pm25': 0,
'nox': 0,
'so2': 0,
},
'day': {
'totalPower': 0,
'fuel': 0,
'co2': 0,
'pm25': 0,
'nox': 0,
'so2': 0,
},
'month': {
'totalPower': 0,
'fuel': 0,
'co2': 0,
'pm25': 0,
'nox': 0,
'so2': 0,
},
'year': {
'totalPower': 0,
'fuel': 0,
'co2': 0,
'pm25': 0,
'nox': 0,
'so2': 0,
},
})
const companyShorePowerBuildData = ref<CompanyShorePowerBuildDataItem | null>(null)
const shipTotalData = ref<any>({
'berthingShips': 0,
'shorePowerShips': 0,
'noShorePowerShips': 0,
})
// 更新时间的函数
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 = 0;
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
// 更新数据更新时间
realtimeDeviceDataTime.value = dayjs().format('YYYY-MM-DD HH:mm:ss')
return 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) => {
if (ship.applyInfo.reason != 0) {
return '异常'
}
if (ship.shorePowerAndShip.status === 9) {
return '正常'
} else {
const shorePower = realtimeDeviceData.value.find(item => item.deviceId === ship.shorePowerAndShip.shorePowerId)
if (!shorePower) {
return '未知'
}
// const shorePower = ShorePowerList.value.find(item => item.shorePowerId === ship.shorePowerAndShip.shorePowerId)
// return shorePower?.storePowerStatus || '故障'
if ([2, 5, 8].includes(shorePower.deviceStatus)) {
return '故障'
} else if (shorePower?.deviceStatus === 6) {
return '在用'
} else {
const Vobj = realtimeDeviceData.value.find(item => item.deviceId === ship.shorePower.voltageDeviceId)
const Cobj = realtimeDeviceData.value.find(item => item.deviceId === ship.shorePower.currentDeviceId)
let capacity = 0
try {
capacity = Number(ship.shorePowerEquipment.capacity)
} catch (error) {
return '获取岸电容量失败'
}
const power = (Vobj?.measureValue ?? 0) * (Cobj?.measureValue ?? 0);
if (power / 1000 > capacity) {
return '在用'
}
return '在用'
}
}
}
const handleGetShipData = async () => {
const shipData = await MapApi.getShipInfo({
harborDistrictId: 1
})
const buildData = shipData.map(item => ({
...item,
shipStatus: getShipStatus(item),
StatusReason: getShipStatus(item) === '故障' ? '跳闸' : '',
}))
shipDataList.value = buildData
return buildData
}
/* 本日实时用量 */
const todayEnergyUsing = computed(() => {
const kwhData = realtimeDeviceData.value.filter(item => item.deviceCode.includes('Kwh')).reduce((sum, item) => sum + item.measureValue, 0)
const dayData = dayDataRes.value.reduce((sum, item) => sum + item.measureValue, 0)
return kwhData - dayData
})
/* 本月实时用量 */
const monthEnergyUsing = computed(() => {
const kwhData = realtimeDeviceData.value.filter(item => item.deviceCode.includes('Kwh')).reduce((sum, item) => sum + item.measureValue, 0)
const monthData = monthDataRes.value.reduce((sum, item) => sum + item.measureValue, 0)
return kwhData - monthData
})
/* 本年实时用量 */
const yearEnergyUsing = computed(() => {
const kwhData = realtimeDeviceData.value.filter(item => item.deviceCode.includes('Kwh')).reduce((sum, item) => sum + item.measureValue, 0)
const yearData = yearDataRes.value.reduce((sum, item) => sum + item.measureValue, 0)
return kwhData - yearData
})
// 监听组件挂载
onMounted(async () => {
dockList.value = await MapApi.getDockIdAndNameList()
berthList.value = await MapApi.getBerthIdAndNameList()
// 延迟启动滚动,确保DOM已渲染
const firstData = await handleGetRealtimeAllData()
const res: RealtimeDeviceData[] = firstData.filter(item => item.deviceCode.includes('Kwh'));
const ids = res.map(item => item.id);
const params = {
ids: ids.join(','),
// year: new Date().getFullYear()
}
yearDataRes.value = await MapApi.getYearDataByIdList(params);
monthDataRes.value = await MapApi.getMonthDataByIdList(params);
dayDataRes.value = await MapApi.getDayDataByIdList(params);
const applyShipCount = await MapApi.getApplyShipCount()
console.log('applyShipCount', applyShipCount)
handleGetlRangeData(firstData)
handleBuildCompanyShorePower(firstData)
const buildComparativeData = await handleBuildTimeComparison()
console.log('buildComparativeData', buildComparativeData)
comparativeData.value = buildComparativeData
await handleGetStorePower()
const tempBuildShipData = await handleGetShipData()
handleBuildCompanyShorePowerYear(tempBuildShipData)
dataLoad.value = true
getRealtimeIntervalId = setInterval(async () => {
const data = await handleGetRealtimeAllData()
if (data) {
handleGetlRangeData(data)
handleBuildCompanyShorePower(data)
handleBuildCompanyShorePowerYear(tempBuildShipData)
}
}, 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 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 handleGoToModule = (moduleType: number) => {
activeHeadGroup.value = moduleType
}
const handleGoBack = () => {
activeHeadGroup.value = 0
}
const handleClickTitle = () => {
if (activeHeadGroup.value === 0) {
activeHeadGroup.value = -1
return
}
activeHeadGroup.value = 0
}
// 从子组件获取的船舶数据
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
}
})
})
const handleGetlRangeData = async (realtimeDeviceData: RealtimeDeviceData[]) => {
try {
const res: RealtimeDeviceData[] = realtimeDeviceData.filter(item => item.deviceCode.includes('Kwh'));
const ids = res.map(item => item.id);
const params = {
ids: ids.join(','),
// year: new Date().getFullYear()
}
if (!params.ids) return;
let yearDataRes: RealtimeDeviceData[] = []
let monthDataRes: RealtimeDeviceData[] = []
let dayDataRes: RealtimeDeviceData[] = []
try {
yearDataRes = await MapApi.getYearDataByIdList(params);
monthDataRes = await MapApi.getMonthDataByIdList(params);
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 monthSum = realTimeSum - Object.values(monthDataRes).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);
// totalPower.value = realTimeSum.toFixed(2)
// co2Reduction.value = (realTimeSum * 670 / 1000000).toFixed(2) // 克转化为吨
// so2Reduction.value = (realTimeSum * 10.5 / 1000000).toFixed(2) // 克转化为吨
// noxReduction.value = (realTimeSum * 18.1 / 1000000).toFixed(2) // 克转化为吨
// pm25Reduction.value = (realTimeSum * 1.46 / 1000000).toFixed(2) // 克转化为吨
// fuelReduction.value = (realTimeSum * 0.22 / 1000).toFixed(2) // 转化为吨
const incrementRealTimeSum = Object.values(res).reduce((acc, item) => acc + item.measureValue, 0);
chartData.value.realtime = {
totalPower: Number(realTimeSum.toFixed(2)),
fuel: Number((realTimeSum * 0.22 / 1).toFixed(2)), // 转化为千克
co2: Number((realTimeSum * 670 / 1000).toFixed(2)), // 克转化为千克
pm25: Number((realTimeSum * 1.46 / 1000).toFixed(2)), // 克转化为千克
nox: Number((realTimeSum * 18.1 / 1000).toFixed(2)), // 克转化为千克
so2: Number((realTimeSum * 10.5 / 1000).toFixed(2)), // 克转化为千克
}
chartData.value.day = {
totalPower: Number(daySum.toFixed(2)),
fuel: Number((daySum * 0.22 / 1).toFixed(2)), // 转化为千克
co2: Number((daySum * 670 / 1000).toFixed(2)), // 克转化为千克
pm25: Number((daySum * 1.46 / 1000).toFixed(2)), // 克转化为千克
nox: Number((daySum * 18.1 / 1000).toFixed(2)), // 克转化为千克
so2: Number((daySum * 10.5 / 1000).toFixed(2)), // 克转化为千克
}
chartData.value.month = {
totalPower: Number(monthSum.toFixed(2)),
fuel: Number((monthSum * 0.22 / 1).toFixed(2)), // 转化为吨
co2: Number((monthSum * 670 / 1000).toFixed(2)), // 克转化为千克
pm25: Number((monthSum * 1.46 / 1000).toFixed(2)), // 克转化为千克
nox: Number((monthSum * 18.1 / 1000).toFixed(2)), // 克转化为千克
so2: Number((monthSum * 10.5 / 1000).toFixed(2)), // 克转化为千克
}
chartData.value.year = {
totalPower: Number(yearSum.toFixed(2)),
fuel: Number((yearSum * 0.22 / 1).toFixed(2)), // 转化为千克
co2: Number((yearSum * 670 / 1000).toFixed(2)), // 克转化为千克
pm25: Number((yearSum * 1.46 / 1000).toFixed(2)), // 克转化为千克
nox: Number((yearSum * 18.1 / 1000).toFixed(2)), // 克转化为千克
so2: Number((yearSum * 10.5 / 1000).toFixed(2)), // 克转化为千克
}
// 记录实时数据到图表数组
const now = new Date()
/* const timestamp = now.toLocaleTimeString()
// 计算各指标值(转换为合适的单位)并保留两位小数
const totalPowerValue = Number(incrementRealTimeSum.toFixed(2))
const co2Value = Number((incrementRealTimeSum * 670 / 1000).toFixed(2)) // 克转化为千克
const so2Value = Number((incrementRealTimeSum * 10.5 / 1000).toFixed(2)) // 克转化为千克
const noxValue = Number((incrementRealTimeSum * 18.1 / 1000).toFixed(2)) // 克转化为千克
const pm25Value = Number((incrementRealTimeSum * 1.46 / 1000).toFixed(2)) // 克转化为千克
const fuelValue = Number((incrementRealTimeSum * 0.22 / 1).toFixed(2)) // 转化为千克 */
// 添加到各图表数据数组
/* totalPowerChartData.value.push({ name: timestamp, value: totalPowerValue })
co2ReductionChartData.value.push({ name: timestamp, value: co2Value })
so2ReductionChartData.value.push({ name: timestamp, value: so2Value })
noxReductionChartData.value.push({ name: timestamp, value: noxValue })
pm25ReductionChartData.value.push({ name: timestamp, value: pm25Value })
fuelReductionChartData.value.push({ name: timestamp, value: fuelValue })
// 保持数组最多包含20个数据点(对应约3分钟,每10秒一条)
const maxDataPoints = 10
totalPowerChartData.value = totalPowerChartData.value.slice(-maxDataPoints)
co2ReductionChartData.value = co2ReductionChartData.value.slice(-maxDataPoints)
so2ReductionChartData.value = so2ReductionChartData.value.slice(-maxDataPoints)
noxReductionChartData.value = noxReductionChartData.value.slice(-maxDataPoints)
pm25ReductionChartData.value = pm25ReductionChartData.value.slice(-maxDataPoints)
fuelReductionChartData.value = fuelReductionChartData.value.slice(-maxDataPoints) */
} catch (error) {
console.error(error)
}
}
const handleBuildCompanyShorePower = (realtimeDeviceData: RealtimeDeviceData[]) => {
const realtimeBuildData = dockList.value.map(dock => {
const children = berthList.value
.filter(berth => berth.dockId === dock.id)
.map(berth => ({
...berth,
...realtimeDeviceData.find(item => (item.deviceId === berth.id) && (item.deviceCode.includes('Kwh')))
}));
// 计算所有children的measureValue总和
const totalPower = children.reduce((sum, child) => {
return sum + (child.measureValue || 0);
}, 0);
return {
...dock,
children,
totalPower
};
});
// console.log(dayDataRes.value)
const dayBuildData = dockList.value.map(dock => {
const children = berthList.value
.filter(berth => berth.dockId === dock.id)
.map(berth => ({
...berth,
...dayDataRes.value.find(item => (item.deviceId === berth.id))
}));
// 计算所有children的measureValue总和
const totalPower = children.reduce((sum, child) => {
return sum + (child.measureValue || 0);
}, 0);
return {
...dock,
children,
totalPower
};
});
const monthBuildData = dockList.value.map(dock => {
const children = berthList.value
.filter(berth => berth.dockId === dock.id)
.map(berth => ({
...berth,
...monthDataRes.value.find(item => (item.deviceId === berth.id))
}));
// 计算所有children的measureValue总和
const totalPower = children.reduce((sum, child) => {
return sum + (child.measureValue || 0);
}, 0);
return {
...dock,
children,
totalPower
};
});
const yearBuildData = dockList.value.map(dock => {
const children = berthList.value
.filter(berth => berth.dockId === dock.id)
.map(berth => ({
...berth,
...yearDataRes.value.find(item => (item.deviceId === berth.id))
}));
// 计算所有children的measureValue总和
const totalPower = children.reduce((sum, child) => {
return sum + (child.measureValue || 0);
}, 0);
return {
...dock,
children,
totalPower
};
});
companyShorePowerBuildData.value = {
realtime: realtimeBuildData,
year: yearBuildData,
month: monthBuildData,
day: dayBuildData,
}
// console.log(buildData);
}
const handleBuildCompanyShorePowerYear = (shipData: any) => {
shipTotalData.value = {
'berthingShips': shipData.length,
'shorePowerShips': shipData.filter(ship => ['在用'].includes(ship.shipStatus || '')).length,
'noShorePowerShips': shipData.filter(ship => !['在用'].includes(ship.shipStatus || '')).length,
}
}
/**
* 构建日、月、年三个维度的环比对比数据
* - 日环比:今日 00:00 至当前 vs 昨日 00:00 至昨日此时
* - 月环比:本月 01 日至当前 vs 上月 01 日至上月同日(对齐天数)
* - 年环比:本年 01-01 至当前 vs 去年 01-01 至去年同日(对齐天数)
*/
const handleBuildTimeComparison = async (): Promise<ComparativeData> => {
const now = dayjs();
/**
* 计算每个分组的周期用量(last.measureValue - first.measureValue 的总和)
*/
const calculatePeriodUsage = (apiResponse: Record<string, { measureValue: number }[]>): number => {
const dataArrays = Object.values(apiResponse).filter(Array.isArray) as { measureValue: number }[][];
return dataArrays.reduce((total, row) => {
if (row.length === 0) return total;
const firstValue = row[0].measureValue;
const lastValue = row[row.length - 1].measureValue;
return total + (lastValue - firstValue);
}, 0);
};
/**
* 安全计算环比增长率:(current - previous) / previous
* - 若 previous 为 0 且 current 也为 0 → 返回 0
* - 若 previous 为 0 但 current > 0 → 返回 null(表示“新增”)
*/
const calculateGrowthRate = (current: number, previous: number): number | null => {
if (previous === 0) {
return current === 0 ? 0 : null; // null 表示无法计算(如从 0 到正数)
}
return (current - previous) / previous;
};
// ===== 构建日环比参数 =====
const todayStart = now.startOf('day').valueOf();
const todayEnd = now.valueOf();
const yesterday = now.subtract(1, 'day');
const yesterdayStart = yesterday.startOf('day').valueOf();
const yesterdayEnd = yesterday.valueOf();
// ===== 构建月环比参数(对齐天数)=====
const thisMonthStart = now.startOf('month').valueOf();
const thisMonthEnd = now.valueOf();
const lastMonth = now.subtract(1, 'month');
const lastMonthStart = lastMonth.startOf('month').valueOf();
// 上月可能没有“今天”这个日期(如今天31号,上月只有30天),取最小值避免跨月
const daysInLastMonth = lastMonth.daysInMonth();
const todayDate = now.date();
const alignedDay = Math.min(todayDate, daysInLastMonth);
const lastMonthEnd = lastMonth.date(alignedDay).endOf('day').valueOf();
// ===== 构建年环比参数(对齐天数)=====
const thisYearStart = now.startOf('year').valueOf();
const thisYearEnd = now.valueOf();
const lastYear = now.subtract(1, 'year');
const lastYearStart = lastYear.startOf('year').valueOf();
// 同样处理闰年/年末问题(如今天是 2 月 29 日,去年不是闰年)
let lastYearEnd: number;
try {
// 尝试设置为去年同月同日
lastYearEnd = lastYear.month(now.month()).date(now.date()).endOf('day').valueOf();
} catch {
// 如果失败(如 2/29 不存在),则取该月最后一天
lastYearEnd = lastYear.month(now.month()).endOf('month').valueOf();
}
// ===== 并发请求所有6个时间段的数据 =====
const [
todayRes,
yesterdayRes,
thisMonthRes,
lastMonthRes,
thisYearRes,
lastYearRes
] = await Promise.all([
MapApi.getByStartAndEndTimeAndTimeType({ start: todayStart, end: todayEnd, timeType: 2 }),
MapApi.getByStartAndEndTimeAndTimeType({ start: yesterdayStart, end: yesterdayEnd, timeType: 2 }),
MapApi.getByStartAndEndTimeAndTimeType({ start: thisMonthStart, end: thisMonthEnd, timeType: 3 }),
MapApi.getByStartAndEndTimeAndTimeType({ start: lastMonthStart, end: lastMonthEnd, timeType: 3 }),
MapApi.getByStartAndEndTimeAndTimeType({ start: thisYearStart, end: thisYearEnd, timeType: 4 }),
MapApi.getByStartAndEndTimeAndTimeType({ start: lastYearStart, end: lastYearEnd, timeType: 4 })
]);
// ===== 计算各周期用量 =====
const todayUsage = calculatePeriodUsage(todayRes);
const yesterdayUsage = calculatePeriodUsage(yesterdayRes);
const thisMonthUsage = calculatePeriodUsage(thisMonthRes);
const lastMonthUsage = calculatePeriodUsage(lastMonthRes);
const thisYearUsage = calculatePeriodUsage(thisYearRes);
const lastYearUsage = calculatePeriodUsage(lastYearRes);
// ===== 返回结构化结果 =====
return {
day: {
growthRate: calculateGrowthRate(todayUsage, yesterdayUsage),
current: { period: now.format('YYYY-MM-DD'), value: todayUsage },
previous: { period: yesterday.format('YYYY-MM-DD'), value: yesterdayUsage }
},
month: {
growthRate: calculateGrowthRate(thisMonthUsage, lastMonthUsage),
current: { period: now.format('YYYY-MM'), value: thisMonthUsage },
previous: { period: lastMonth.format('YYYY-MM'), value: lastMonthUsage }
},
year: {
growthRate: calculateGrowthRate(thisYearUsage, lastYearUsage),
current: { period: now.format('YYYY'), value: thisYearUsage },
previous: { period: lastYear.format('YYYY'), value: lastYearUsage }
}
};
};
</script>
<style lang="scss">
.cesium-viewer-bottom {
display: none !important;
}
.loading {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 0.5);
z-index: 9999;
}
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.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;
padding-right: 900px;
background: rgba(8, 15, 30, 0.6);
// backdrop-filter: blur(10px);
// -webkit-backdrop-filter: blur(10px);
border-bottom: 1px solid rgba(30, 120, 255, 0.2);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
.head-title {
// width: 420px;
font-size: 36px;
color: rgba(20, 200, 255, 1);
text-align: center;
font-weight: bold;
text-align: left;
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
}
.head-group {
display: flex;
flex-wrap: wrap;
gap: 10px;
.head-group-item {
font-size: 16px;
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: #FF9800;
/* 红色 */
box-shadow: 0 0 5px rgba(255, 152, 0, 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;
}
.time-range-btn {
padding: 4px 12px;
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 4px;
background: transparent;
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);
}
.time-range-btn.active {
background: rgba(76, 175, 80, 0.8);
border-color: #4CAF50;
color: #fff;
}
</style>