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.
1875 lines
59 KiB
1875 lines
59 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"
|
|
:initial-time-range="selectedTimeRange" v-if="dataLoad" :handleGoBack="handleGoBack" />
|
|
|
|
<!-- 港口岸电使用情况单数据 -->
|
|
<ShorePowerUsageSingleData v-if="activeHeadGroup === 5 && dataLoad"
|
|
:realtime-device-data-time="realtimeDeviceDataTime" :realtime-device-data="realtimeDeviceData"
|
|
:active-head-group="activeHeadGroup" :initial-time-range="selectedTimeRange" :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)
|
|
const selectedTimeRange = ref<string>('day')
|
|
|
|
// 定义子组件类型
|
|
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 allPortAreaCount = ref<number>(0)
|
|
|
|
|
|
|
|
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>({
|
|
'allPortAreaCount': 0,
|
|
'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);
|
|
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, timeRange?: string) => {
|
|
activeHeadGroup.value = moduleType
|
|
// 如果传递了时间范围参数且目标是模块1(港区岸电使用情况),则存储该参数
|
|
if ([1, 5].includes(moduleType) && timeRange) {
|
|
// 更新ShorePowerUsage组件的时间范围
|
|
console.log('timeRange', timeRange)
|
|
|
|
// 我们需要通过某种方式将这个值传递给ShorePowerUsage组件
|
|
selectedTimeRange.value = timeRange
|
|
}
|
|
}
|
|
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 = async (shipData: any) => {
|
|
const applyShipCount = await MapApi.getApplyShipCount() as number
|
|
shipTotalData.value = {
|
|
'allPortAreaCount': applyShipCount,
|
|
'berthingShips': shipData.length,
|
|
'shorePowerShips': shipData.filter(ship => ['在用'].includes(ship.shipStatus || '')).length,
|
|
'noShorePowerShips': applyShipCount - shipData.filter(ship => ['在用'].includes(ship.shipStatus || '')).length,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 构建日、月、年三个维度的环比对比数据和同比数据
|
|
* - 日环比:今日 00:00 至当前 vs 昨日 00:00 至昨日此时
|
|
* - 日同比:今日 00:00 至当前 vs 去年今日 00:00 至去年此时
|
|
* - 月环比:本月 01 日至当前 vs 上月 01 日至上月同日(对齐天数)
|
|
* - 月同比:本月 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 lastYearToday = now.subtract(1, 'year');
|
|
const lastYearTodayStart = lastYearToday.startOf('day').valueOf();
|
|
const lastYearTodayEnd = lastYearToday.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 lastYearMonth = now.subtract(1, 'year');
|
|
const lastYearMonthStart = lastYearMonth.startOf('month').valueOf();
|
|
// 同样处理闰年/年末问题
|
|
const daysInLastYearMonth = lastYearMonth.daysInMonth();
|
|
const lastYearAlignedDay = Math.min(todayDate, daysInLastYearMonth);
|
|
const lastYearMonthEnd = lastYearMonth.date(lastYearAlignedDay).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();
|
|
}
|
|
|
|
// ===== 并发请求所有8个时间段的数据 =====
|
|
const [
|
|
todayRes,
|
|
yesterdayRes,
|
|
lastYearTodayRes,
|
|
thisMonthRes,
|
|
lastMonthRes,
|
|
lastYearMonthRes,
|
|
thisYearRes,
|
|
lastYearRes
|
|
] = await Promise.all([
|
|
MapApi.getByStartAndEndTimeAndTimeType({ start: todayStart, end: todayEnd, timeType: 2 }),
|
|
MapApi.getByStartAndEndTimeAndTimeType({ start: yesterdayStart, end: yesterdayEnd, timeType: 2 }),
|
|
MapApi.getByStartAndEndTimeAndTimeType({ start: lastYearTodayStart, end: lastYearTodayEnd, timeType: 2 }),
|
|
MapApi.getByStartAndEndTimeAndTimeType({ start: thisMonthStart, end: thisMonthEnd, timeType: 3 }),
|
|
MapApi.getByStartAndEndTimeAndTimeType({ start: lastMonthStart, end: lastMonthEnd, timeType: 3 }),
|
|
MapApi.getByStartAndEndTimeAndTimeType({ start: lastYearMonthStart, end: lastYearMonthEnd, 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 lastYearTodayUsage = calculatePeriodUsage(lastYearTodayRes);
|
|
const thisMonthUsage = calculatePeriodUsage(thisMonthRes);
|
|
const lastMonthUsage = calculatePeriodUsage(lastMonthRes);
|
|
const lastYearMonthUsage = calculatePeriodUsage(lastYearMonthRes);
|
|
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 }
|
|
},
|
|
dayYearOnYear: {
|
|
growthRate: calculateGrowthRate(todayUsage, lastYearTodayUsage),
|
|
current: { period: now.format('YYYY-MM-DD'), value: todayUsage },
|
|
previous: { period: lastYearToday.format('YYYY-MM-DD'), value: lastYearTodayUsage }
|
|
},
|
|
month: {
|
|
growthRate: calculateGrowthRate(thisMonthUsage, lastMonthUsage),
|
|
current: { period: now.format('YYYY-MM'), value: thisMonthUsage },
|
|
previous: { period: lastMonth.format('YYYY-MM'), value: lastMonthUsage }
|
|
},
|
|
monthYearOnYear: {
|
|
growthRate: calculateGrowthRate(thisMonthUsage, lastYearMonthUsage),
|
|
current: { period: now.format('YYYY-MM'), value: thisMonthUsage },
|
|
previous: { period: lastYearMonth.format('YYYY-MM'), value: lastYearMonthUsage }
|
|
},
|
|
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>
|
|
|