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.

683 lines
21 KiB

<template>
<div class="company-shore-power">
3 weeks ago
<div class="left" style="width: 800px;" :style="{ width: selectedShorePowerItem ? '800px' : '420px' }">
3 weeks ago
<div style="display: flex; gap: 12px; height: 100%;">
3 weeks ago
<div class="card digital-twin-card--deep-blue ">
<div style="display: flex; align-items: center; justify-content: space-between;">
<div class="card-title">
<div class="vertical-line"></div>
<img src="@/assets/svgs/data.svg" class="title-icon" />
<span class="title-text">岸电箱状态</span>
</div>
<input class="search-container" type="text" placeholder="搜索岸电设备" v-model="storeSearchKeyword"
@input="handlStoreSearch" />
</div>
<div class=" card-content">
<div class="ship-table">
<div class="ship-table-header">
<div class="ship-table-column ship-name-header">岸电箱名称</div>
<div class="ship-table-column ship-status-header">状态</div>
<div class="ship-table-column ship-status-header">历史</div>
</div>
<div class="ship-table-body">
<div v-for="shorepower in filteredShorePowerList" :key="shorepower.id" class="ship-table-row"
@click="handleSelectShorePower(shorepower)">
<div class="ship-table-column ship-name">
<div class="ship-icon"></div>
<span class="ship-name-text">{{ shorepower.name }}</span>
</div>
<div class="ship-table-column ship-status">
1 week ago
<div class="status-tag"
:class="getStatusClass(getOperationTypeLabel(shorePowerStatus(shorepower), SHORE_POWER_FIRST_STATUS))">
{{ getOperationTypeLabel(shorePowerStatus(shorepower), SHORE_POWER_FIRST_STATUS) }}
3 weeks ago
</div>
<!-- <div class="status-tag" :class="getStatusClass('空闲')">
空闲
</div>
<div class="status-tag" :class="getStatusClass('故障')">
故障
</div> -->
</div>
<div class="ship-table-column ship-status-header">
<el-button type="primary" link @click.stop="showShorePowerHistory(shorepower)">查看</el-button>
</div>
</div>
</div>
</div>
</div>
</div>
3 weeks ago
<div v-if="selectedShorePowerItem" class="card digital-twin-card--deep-blue">
<div style="display: flex; align-items: center; justify-content: space-between;">
<div class="card-title">
<div class="vertical-line"></div>
<img src="@/assets/svgs/data.svg" class="title-icon" />
<span class="title-text">岸电箱详情</span>
</div>
<el-button class="close-btn" type="text" @click="closeShorePowerDetail">×</el-button>
<!-- <div style="ma">
<el-button type="primary" @click="handleOpenHistory">查看历史记录</el-button>
</div> -->
4 weeks ago
</div>
3 weeks ago
<div class="card-content">
<div class="ship-detail">
<div class="detail-item">
<span class="label">名称:</span>
<span class="value">{{ selectedShorePowerItem?.name }}</span>
</div>
<div class="detail-item">
<span class="label">位置:</span>
<span class="value">{{ selectedShorePowerItem?.position }}</span>
</div>
<div class="detail-item">
<span class="label">电压:</span>
<span class="value">{{ selectedShorePowerItem?.shorePowerEquipmentInfo?.voltage }}</span>
</div>
<div class="detail-item">
<span class="label">频率:</span>
<span class="value">{{ selectedShorePowerItem?.shorePowerEquipmentInfo?.frequency }} </span>
</div>
<div class="detail-item">
<span class="label">容量:</span>
<span class="value">{{ selectedShorePowerItem?.shorePowerEquipmentInfo?.capacity }}</span>
</div>
<div class="detail-item">
<span class="label">当前电压:</span>
<span class="value">{{ getValueById(realtimeDeviceData, selectedShorePowerItem.voltageDeviceId,
'measureValue') }}</span>
</div>
<div class="detail-item">
<span class="label">当前电流:</span>
<span class="value">{{ getValueById(realtimeDeviceData, selectedShorePowerItem.currentDeviceId,
'measureValue') }}</span>
</div>
<div class="detail-item">
<span class="label">当前频率:</span>
<span class="value">{{ getValueById(realtimeDeviceData, selectedShorePowerItem.frequencyDeviceId,
'measureValue') }}</span>
</div>
<div class="detail-item">
<span class="label">当前总用量:</span>
<span class="value">{{ getValueById(realtimeDeviceData, selectedShorePowerItem.totalPowerDeviceId,
'measureValue') }}</span>
</div>
<div class="detail-item">
<span class="label">当前状态:</span>
1 week ago
<span class="value">{{ getOperationTypeLabel(shorePowerStatus(selectedShorePowerItem),
SHORE_POWER_FIRST_STATUS,
) }}({{ getOperationTypeLabel(shorePowerStatus(selectedShorePowerItem),
SHORE_POWER_SECOND_STATUS_MAP,
) }})</span>
3 weeks ago
</div>
4 weeks ago
</div>
3 weeks ago
</div>
</div>
4 weeks ago
</div>
</div>
3 weeks ago
<div class="right" style="width: 1000px;">
<!-- 码头信息卡片 -->
<div class="card digital-twin-card--deep-blue">
<div class="card-title" style="display: flex; align-items: center; justify-content: space-between;">
<div class="card-title">
<div class="vertical-line"></div>
<img src="@/assets/svgs/data.svg" class="title-icon" />
<span class="title-text">码头与泊位信息</span>
</div>
<!-- 返回上个页面 -->
<el-button type="primary" @click.stop="handleGoBack()">返回</el-button>
<!-- <div>
<el-date-picker type="daterange" range-separator="" start-placeholder="开始日期" end-placeholder="结束日期"
format="YYYY-MM-DD" value-format="YYYY-MM-DD" class="date-range-picker" />
</div> -->
</div>
<div class="card-content">
<div v-for="company in companyComparisonData" :key="company.name" class="company-section">
<div class="data-update-time">起始时间: {{ getStartTimeByRange(getCompanyTimeRange(company.id)) }} 更新时间: {{
realtimeDeviceDataTime }}</div>
<div class="company-header">
<div class="company-info">
<div class="company-name">{{ company.name }}</div>
3 weeks ago
<div class="company-berth-count">泊位数: {{ company.shorePowerCount || 0 }}</div>
3 weeks ago
</div>
<div class="time-range-container">
<button v-for="option in timeRangeOptions" :key="option.value"
:class="['time-range-btn', { active: getCompanyTimeRange(company.id) === option.value }]"
@click.stop="() => handleTimeRangeChange(company.id, option.value)">
{{ option.label }}
</button>
2 weeks ago
<div class="company-total">总量: {{ handGetDeviceData(company.shorePowerIds,
getCompanyTimeRange(company.id)) }}</div>
3 weeks ago
</div>
</div>
3 weeks ago
<div class="shorepower-container">
<div class="diste-box" v-for="(disteItem, index) in (company.distributionList || [])" :key="index">
<div class="diste-name">{{ disteItem.name }}</div>
3 weeks ago
3 weeks ago
<div class="shorepower-box">
<div class="shorepower-item" v-for="(shorePower, shorePowerIndex) in (disteItem.shorePowerList || [])"
:key="shorePowerIndex">
<div class="shorepower-value" @click="handleClickhorePowerValue(shorePower)">{{
2 weeks ago
handGetDeviceData(shorePower.totalPowerDeviceId, getCompanyTimeRange(company.id)) || 0 }}</div>
3 weeks ago
<div class="shorepower-label" @click="handleClickShorePowerName(shorePower)">{{ shorePower.name }}
</div>
</div>
</div>
3 weeks ago
</div>
3 weeks ago
3 weeks ago
</div>
</div>
</div>
</div>
</div>
3 weeks ago
<ship-history-dialog v-model="historyVisible.visible" :ship-param="historyVisible.searchParams"
:realtime-device-data="realtimeDeviceData" />
</div>
</template>
<script setup lang="ts">
3 weeks ago
import { ref, computed } from 'vue'
4 weeks ago
import { MapApi } from "@/api/shorepower/map";
import ShipHistoryDialog from './ShipHistoryDialog.vue';
2 weeks ago
import { CompanyShorePowerData, RealtimeDeviceData, ShipRespVo, ShorePowerBerth } from '@/types/shorepower';
4 weeks ago
import { getValueById } from './utils';
1 week ago
import { getOperationTypeLabel, SHORE_POWER_FIRST_STATUS, SHORE_POWER_SECOND_STATUS_MAP } from './dictionaryTable';
// 定义组件属性
2 weeks ago
4 weeks ago
const historyVisible = ref({
visible: false,
searchParams: {
shipId: 0 as number | null,
ids: [] as number[] | null,
type: 1 as number
}
})
4 weeks ago
interface Props {
realtimeDeviceData: RealtimeDeviceData[];
3 weeks ago
realtimeDeviceDataTime: string;
4 weeks ago
shorePowerList: ShorePowerBerth[];
3 weeks ago
handleGoBack: () => void;
2 weeks ago
yearData: any[];
monthData: any[];
dayData: any[];
2 weeks ago
companyComparisonData: CompanyShorePowerData[];
4 weeks ago
}
const props = defineProps<Props>()
const selectedShorePowerItem = ref<ShorePowerBerth>()
3 weeks ago
const shorepowerSelectedItem = ref<ShorePowerBerth & { position: string; } | null>(null)
const storeSearchKeyword = ref('')
// 时间范围选项定义
interface TimeRangeOption {
value: 'day' | 'month' | 'year' | 'realtime';
label: string;
}
const timeRangeOptions: TimeRangeOption[] = [
3 weeks ago
{ value: 'day', label: '当日' },
{ value: 'month', label: '当月' },
{ value: 'year', label: '当年' },
{ value: 'realtime', label: '汇总' },
3 weeks ago
]
const closeShorePowerDetail = () => {
selectedShorePowerItem.value = undefined
}
// 处理岸电箱搜索输入
const handlStoreSearch = () => {
// 输入处理逻辑(如果需要额外处理可以在这里添加)
}
1 week ago
const shorePowerStatus = (item: ShorePowerBerth) => {
if (!item.totalPowerDeviceId) {
return ''
}
const status = getDeviceStatus(item.totalPowerDeviceId)
if (status === null) {
return ''
}
return status
}
/* 用户实时设备状态 */
const getDeviceStatus = (deviceId: number) => {
const data = props.realtimeDeviceData.find(item => item.deviceId === deviceId)
if (!data) {
return null
}
return data.deviceStatus
}
3 weeks ago
const getStatusClass = (status: string | undefined) => {
switch (status) {
case '正常':
1 week ago
return 'status-default'
3 weeks ago
case '在用':
return 'status-normal'
case '故障':
return 'status-fault'
default:
return 'status-normal'
}
}
// 过滤后的岸电箱数据
const filteredShorePowerList = computed(() => {
if (!storeSearchKeyword.value) {
return props.shorePowerList
}
return props.shorePowerList.filter(shorepower =>
shorepower.name.toLowerCase().includes(storeSearchKeyword.value.toLowerCase())
)
})
// 定义事件
const emit = defineEmits<{
(e: 'switch-ship', ship: ShipRespVo): void;
(e: 'item-click', item: any): void;
// handleSelectItem(ship)
}>()
const handleSelectShorePower = async (shorepower: ShorePowerBerth & { position: string }) => {
// selectedItem.value = shorepower
shorepowerSelectedItem.value = shorepower
selectedShorePowerItem.value = shorepower
emit('item-click', {
type: 'shorepower_box',
item: shorepower
})
// const data = await MapApi.getRealtimeDataByIdList({ ids: shorepower.totalPowerDeviceId })
// console.log('voltageDeviceId', data)
}
// 为每个公司维护独立的时间范围状态
const companyTimeRanges = ref<Record<number, 'realtime' | 'day' | 'month' | 'year'>>({})
// 获取指定公司的时间范围设置,默认为'day'
const getCompanyTimeRange = (companyId: number) => {
return companyTimeRanges.value[companyId] || 'day'
}
// 根据时间粒度计算起始时间
const getStartTimeByRange = (range: 'realtime' | 'day' | 'month' | 'year') => {
const now = new Date();
switch (range) {
case 'day':
// 当天的 00:00:00
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
return formatDate(today);
case 'month':
// 当月第一天的 00:00:00
const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
return formatDate(firstDayOfMonth);
case 'year':
// 当年第一天的 00:00:00
const firstDayOfYear = new Date(now.getFullYear(), 0, 1);
return formatDate(firstDayOfYear);
case 'realtime':
// 固定起始时间,可以根据实际需求修改
return '2020-01-01 00:00:00';
default:
return formatDate(new Date(now.getFullYear(), now.getMonth(), now.getDate()));
}
}
// 格式化日期为 YYYY-MM-DD HH:mm:ss 格式
const formatDate = (date: Date) => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day} 00:00:00`;
}
// 处理时间范围选择
const handleTimeRangeChange = (companyId: number, range: 'realtime' | 'day' | 'month' | 'year') => {
companyTimeRanges.value[companyId] = range
// 这里可以添加根据时间范围切换数据源的逻辑
}
4 weeks ago
/* 点击泊位box */
2 weeks ago
const handleClickShorePowerName = (data: CompanyShorePowerData) => {
3 weeks ago
const shorePower = props.shorePowerList.find(item => item.id === data.id)
4 weeks ago
selectedShorePowerItem.value = shorePower
console.log(shorePower)
}
2 weeks ago
const handleClickhorePowerValue = (data: CompanyShorePowerData) => {
3 weeks ago
const shorePower = props.shorePowerList.find(item => item.id === data.id)
if (!shorePower) {
return
4 weeks ago
}
3 weeks ago
showShorePowerHistory(shorePower)
/* selectedShorePowerItem.value = shorePower
console.log(shorePower) */
4 weeks ago
}
3 weeks ago
const showShorePowerHistory = (berth: ShorePowerBerth) => {
4 weeks ago
historyVisible.value = {
visible: true,
searchParams: {
shipId: null,
ids: [berth.id],
type: 4,
}
}
}
2 weeks ago
// const companyComparisonData = ref<companyData[]>([])
3 weeks ago
// 创建一个响应式的设备数据映射,以提高查找效率并确保响应性
const deviceDataMap = computed(() => {
const map = new Map<number, RealtimeDeviceData>();
props.realtimeDeviceData.forEach(item => {
map.set(item.deviceId, item);
});
return map;
});
2 weeks ago
const handGetDeviceData = (deviceId: number | number[], range: 'realtime' | 'day' | 'month' | 'year') => {
3 weeks ago
let totalValue = 0;
2 weeks ago
let rangData: RealtimeDeviceData[] = []
if (range === 'realtime') {
rangData = props.realtimeDeviceData
} else if (range === 'day') {
rangData = props.dayData
} else if (range === 'month') {
rangData = props.monthData
} else if (range === 'year') {
rangData = props.yearData
}
3 weeks ago
if (Array.isArray(deviceId)) {
2 weeks ago
// If deviceId is an array, sum all the measure values and subtract start values
3 weeks ago
deviceId.forEach(id => {
const deviceData = deviceDataMap.value.get(id);
2 weeks ago
const startData = rangData.find(item => item.deviceId === id)
3 weeks ago
if (deviceData?.measureValue) {
totalValue += deviceData.measureValue;
}
2 weeks ago
// For non-realtime ranges, subtract the start value
if (range !== 'realtime' && startData?.measureValue) {
totalValue -= startData.measureValue;
}
3 weeks ago
});
} else {
// If deviceId is a single number, get its measure value
const deviceData = deviceDataMap.value.get(deviceId);
2 weeks ago
const startData = rangData.find(item => item.deviceId === deviceId)
2 weeks ago
3 weeks ago
if (deviceData?.measureValue) {
totalValue = deviceData.measureValue;
}
2 weeks ago
if (range !== 'realtime' && startData?.measureValue) {
totalValue -= startData.measureValue
}
3 weeks ago
}
return totalValue.toFixed(2);
}
2 weeks ago
// const handleGetBuildData = async () => {
// const dockList = await MapApi.getDockIdAndNameList()
// // const berthList = await MapApi.getBerthIdAndNameList()
// const distributionList = groupByShorePowerEquipmentId(props.shorePowerList)
// const buildData = dockList.map(dock => {
// const filterData = distributionList.filter(item => item.dockId === dock.id)
// const shorePowerCount = filterData.reduce((total, item) => {
// return total + item.shorePowerList.length
// }, 0)
// return {
// ...dock,
// shorePowerCount: shorePowerCount, // 岸电箱总数
// shorePowerIds: filterData.map(item => item.shorePowerList.map(shorePower => shorePower.id)).flat(),
// distributionList: filterData
// }
// })
// companyComparisonData.value = buildData
// /* 转换函数 */
// function groupByShorePowerEquipmentId(data) {
// const map = new Map();
// for (const item of data) {
// const { shorePowerEquipmentInfo, ...rest } = item;
// const id = shorePowerEquipmentInfo.id;
// if (!map.has(id)) {
// map.set(id, {
// ...shorePowerEquipmentInfo,
// shorePowerList: []
// });
// }
// map.get(id).shorePowerList.push(rest);
// }
// return Array.from(map.values());
// }
// }
4 weeks ago
onMounted(() => {
2 weeks ago
// handleGetBuildData()
4 weeks ago
})
</script>
3 weeks ago
<style scoped lang="scss">
.company-shore-power {
display: flex;
height: 100%;
gap: 10px;
}
1 month ago
.left {
4 weeks ago
/* display: flex;
1 month ago
flex-direction: column;
height: 100%;
4 weeks ago
gap: 10px; */
1 month ago
}
.card {
display: flex;
flex-direction: column;
height: 100%;
}
.card-content {
flex: 1;
height: 100%;
min-height: 0;
4 weeks ago
padding-bottom: 50px;
4 weeks ago
overflow-y: auto;
1 month ago
}
4 weeks ago
/* 总览网格样式 */
3 weeks ago
/* .overview-grid {
4 weeks ago
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
gap: 8px;
padding: 4px;
1 month ago
height: 100%;
3 weeks ago
} */
.shorepower-container {
1 month ago
display: flex;
3 weeks ago
gap: 8px;
flex-wrap: wrap;
.diste-box {
flex-shrink: 0;
display: inline-block;
margin-bottom: 8px;
.diste-name {
height: 28px;
background-color: rgba(0, 112, 192, 0.579);
border-radius: 6px 6px 0 0;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
font-weight: bold;
color: #fff;
padding: 4px;
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
}
}
4 weeks ago
3 weeks ago
.shorepower-box {
display: flex;
gap: 8px;
// flex-wrap: wrap;
}
4 weeks ago
3 weeks ago
.shorepower-item {
display: inline-flex;
width: 224px;
// display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 4px;
background-color: rgba(30, 120, 255, 0.15);
border-radius: 0 0 8px 8px;
}
.shorepower-label {
text-align: center;
font-size: 24px;
color: #ccc;
margin-bottom: 2px;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
// 上浮
transform: translateY(-2px);
}
}
4 weeks ago
3 weeks ago
.shorepower-value {
font-size: 36px;
font-weight: bold;
color: #1296db;
text-shadow: 0 0 5px rgba(0, 0, 0, 0.5);
margin-bottom: 2px;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
// 上浮
transform: translateY(-2px);
}
}
4 weeks ago
}
3 weeks ago
4 weeks ago
/* 企业区块样式 */
.company-section {
margin-bottom: 16px;
padding-bottom: 16px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.company-section:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
/* 企业头部样式 */
.company-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
padding: 0 4px;
}
3 weeks ago
.company-info {
display: flex;
align-items: baseline;
gap: 2px
}
4 weeks ago
.company-name {
font-size: 24px;
font-weight: bold;
color: #fff;
}
3 weeks ago
.company-berth-count {
2 weeks ago
font-size: 18px;
color: #eee;
3 weeks ago
margin-top: 4px;
}
4 weeks ago
.company-total {
font-size: 24px;
font-weight: bold;
color: #1296db;
background-color: rgba(18, 150, 219, 0.1);
padding: 2px 8px;
border-radius: 4px;
}
4 weeks ago
3 weeks ago
.data-update-time {
margin-left: 6px;
2 weeks ago
font-size: 24px;
3 weeks ago
color: #aaa;
white-space: nowrap;
}
4 weeks ago
/* 自定义滚动条样式 */
.card-content::-webkit-scrollbar {
width: 6px;
}
.card-content::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
border-radius: 3px;
}
.card-content::-webkit-scrollbar-thumb {
background: rgba(17, 138, 237, 0.7);
border-radius: 3px;
}
.card-content::-webkit-scrollbar-thumb:hover {
background: rgba(17, 138, 237, 1);
}
3 weeks ago
.time-range-container {
display: flex;
align-items: center;
gap: 4px;
}
.search-container {
height: 100%;
width: 200px;
border-radius: 4px;
border: 1px solid rgb(10, 130, 170);
color: #FFF;
padding: 0 10px;
margin-bottom: 8px;
background-color: rgba(255, 255, 255, 0.1);
}
.close-btn {
width: 36px;
height: 36px;
margin-bottom: 8px;
}
</style>