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.
194 lines
6.8 KiB
194 lines
6.8 KiB
|
4 weeks ago
|
import { RealtimeDeviceData, ShipRespVo } from "@/types/shorepower";
|
||
|
|
import dayjs from "dayjs";
|
||
|
|
import { MapApi } from "@/api/shorepower/map";
|
||
|
|
/**
|
||
|
|
* 将毫秒时间戳格式化为 'YYYY/MM/DD HH:mm:ss' 格式
|
||
|
|
* @param timestamp 毫秒时间戳
|
||
|
|
* @returns 格式化后的时间字符串
|
||
|
|
*/
|
||
|
|
export const formatTimestamp = (
|
||
|
|
timestamp: number | null | undefined,
|
||
|
|
format = 'YYYY/MM/DD HH:mm:ss'
|
||
|
|
): string => {
|
||
|
|
if (!timestamp) return '';
|
||
|
|
// 处理秒级时间戳(10位数字)
|
||
|
|
if (timestamp < 1e12) {
|
||
|
|
timestamp *= 1000;
|
||
|
|
}
|
||
|
|
return dayjs(timestamp).format(format);
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 将两个时间戳之间的差值格式化为 "X小时Y分钟Z秒"
|
||
|
|
* @param {number} startTime - 开始时间戳(毫秒)
|
||
|
|
* @param {number} endTime - 结束时间戳(毫秒)
|
||
|
|
* @returns {string} 格式化后的时间字符串,如 "2小时30分钟15秒"
|
||
|
|
*/
|
||
|
|
export function formatDuration(startTime?: number, endTime?: number): string {
|
||
|
|
if (!startTime || !endTime) return '--';
|
||
|
|
const diffMs = Math.max(0, endTime - startTime);
|
||
|
|
const totalSeconds = Math.floor(diffMs / 1000);
|
||
|
|
|
||
|
|
const hours = Math.floor(totalSeconds / 3600);
|
||
|
|
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
||
|
|
const seconds = totalSeconds % 60;
|
||
|
|
|
||
|
|
let result = '';
|
||
|
|
if (hours > 0) result += `${hours}小时`;
|
||
|
|
if (minutes > 0) result += `${minutes}分钟`;
|
||
|
|
if (seconds > 0 || result === '') result += `${seconds}秒`;
|
||
|
|
|
||
|
|
return result || '0秒';
|
||
|
|
}
|
||
|
|
|
||
|
|
// 岸电使用文本构建
|
||
|
|
export function showStatus(ship: ShipRespVo, realtimeDeviceData?: RealtimeDeviceData[]): { statusClass: string, status: string } | null {
|
||
|
|
const { usageRecordInfo, applyInfo } = ship;
|
||
|
|
if (!applyInfo || !usageRecordInfo || !usageRecordInfo.beginTime) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
if (applyInfo.reason == 0 && usageRecordInfo && usageRecordInfo.beginTime) {
|
||
|
|
|
||
|
|
const start = new Date(usageRecordInfo.beginTime);
|
||
|
|
const end = usageRecordInfo.endTime ? new Date(usageRecordInfo.endTime) : new Date();
|
||
|
|
|
||
|
|
// 校验日期有效性
|
||
|
|
if (isNaN(start.getTime()) || isNaN(end.getTime())) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 校验时间顺序
|
||
|
|
if (end < start) {
|
||
|
|
return null; // 或抛出错误、记录日志等
|
||
|
|
}
|
||
|
|
|
||
|
|
// 计算总毫秒差
|
||
|
|
const diffMs = end.getTime() - start.getTime();
|
||
|
|
|
||
|
|
// 转换为秒
|
||
|
|
const totalSeconds = Math.floor(diffMs / 1000);
|
||
|
|
|
||
|
|
const hours = Math.floor(totalSeconds / 3600);
|
||
|
|
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
||
|
|
const seconds = totalSeconds % 60;
|
||
|
|
|
||
|
|
let useValue = 0;
|
||
|
|
|
||
|
|
// 检查是否有结束读数
|
||
|
|
const hasEndReading = usageRecordInfo.powerEndManualReading || usageRecordInfo.powerEndSystemReading;
|
||
|
|
|
||
|
|
if (hasEndReading) {
|
||
|
|
// 有结束读数,使用常规计算方式
|
||
|
|
// 优先计算人工差值
|
||
|
|
const manualDiff = usageRecordInfo.powerStartManualReading !== undefined && usageRecordInfo.powerEndManualReading !== undefined
|
||
|
|
? usageRecordInfo.powerEndManualReading - usageRecordInfo.powerStartManualReading
|
||
|
|
: null;
|
||
|
|
// 然后计算系统差值
|
||
|
|
const systemDiff = usageRecordInfo.powerStartSystemReading !== undefined && usageRecordInfo.powerEndSystemReading !== undefined
|
||
|
|
? usageRecordInfo.powerEndSystemReading - usageRecordInfo.powerStartSystemReading
|
||
|
|
: null;
|
||
|
|
// 使用人工差值优先,然后系统差值,最后默认0
|
||
|
|
useValue = manualDiff ?? systemDiff ?? 0;
|
||
|
|
} else {
|
||
|
|
// 没有结束读数,使用实时数据计算
|
||
|
|
const deviceId = ship.shorePower?.totalPowerDeviceId;
|
||
|
|
if (deviceId) {
|
||
|
|
try {
|
||
|
|
// 调用API获取实时数据
|
||
|
|
const measureValueStr = getValueById(realtimeDeviceData || [], deviceId,
|
||
|
|
'measureValue')
|
||
|
|
if (measureValueStr !== undefined) {
|
||
|
|
|
||
|
|
// 优先使用人工开始读数,然后系统开始读数
|
||
|
|
const startReading = usageRecordInfo.powerStartManualReading ?? usageRecordInfo.powerStartSystemReading ?? 0;
|
||
|
|
const measureValue = typeof measureValueStr === 'string' ? parseFloat(measureValueStr) : measureValueStr;
|
||
|
|
useValue = measureValue - startReading;
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('获取实时数据失败:', error);
|
||
|
|
useValue = 0;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
statusClass: 'status-using',
|
||
|
|
status: `使用岸电${hours}小时${minutes}分钟${seconds}秒,${useValue.toFixed(2)}kWH`
|
||
|
|
}
|
||
|
|
} else if (applyInfo.reason != 0) {
|
||
|
|
// 状态码映射表 - 包含状态文本和颜色类型
|
||
|
|
const statusMap: Record<string, { text: string; colorType: string }> = {
|
||
|
|
'-1': { text: '未知错误', colorType: 'default' },
|
||
|
|
'1': { text: '岸电用电接口不匹配', colorType: 'default' },
|
||
|
|
'2': { text: '岸电设施电压/频率不匹配', colorType: 'success' },
|
||
|
|
'3': { text: '电缆长度不匹配', colorType: 'success' },
|
||
|
|
'4': { text: '气象因素禁止作业', colorType: 'success' },
|
||
|
|
'5': { text: '船电设施损坏', colorType: 'warning' },
|
||
|
|
'6': { text: '岸电设施维护中', colorType: 'warning' },
|
||
|
|
'7': { text: '无受电设备', colorType: 'danger' },
|
||
|
|
'8': { text: '拒绝使用岸电', colorType: 'danger' },
|
||
|
|
'9': { text: '其他', colorType: 'danger' }
|
||
|
|
}
|
||
|
|
|
||
|
|
const reasonKey = applyInfo.reason?.toString() || '-1'
|
||
|
|
const statusInfo = statusMap[reasonKey] || { text: applyInfo.reason, colorType: 'default' }
|
||
|
|
|
||
|
|
// 根据颜色类型设置对应的状态类
|
||
|
|
let statusClass = 'status-cable'
|
||
|
|
switch (statusInfo.colorType) {
|
||
|
|
case 'danger':
|
||
|
|
statusClass = 'status-danger'
|
||
|
|
break
|
||
|
|
case 'warning':
|
||
|
|
statusClass = 'status-warning'
|
||
|
|
break
|
||
|
|
case 'success':
|
||
|
|
statusClass = 'status-success'
|
||
|
|
break
|
||
|
|
default:
|
||
|
|
statusClass = 'status-cable'
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
statusClass: statusClass,
|
||
|
|
status: statusInfo.text
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
return {
|
||
|
|
statusClass: '',
|
||
|
|
status: 'unknown'
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 根据 id 查找对象数组中的指定字段值,并保留两位小数
|
||
|
|
* @param list - 对象数组,每个对象必须包含 id 字段
|
||
|
|
* @param id - 要查找的 id 值
|
||
|
|
* @param valueField - 要返回的字段名(必须是对象中除 id 外的 key)
|
||
|
|
* @returns 字段值,未找到则返回 undefined
|
||
|
|
*/
|
||
|
|
export function getValueById<T extends { id: number | string }, K extends keyof Omit<T, 'id'>>(
|
||
|
|
list: T[],
|
||
|
|
id: T['id'],
|
||
|
|
valueField: K
|
||
|
|
): T[K] extends number | string ? string | undefined : T[K] | undefined {
|
||
|
|
const item = list.find(item => item.id === id);
|
||
|
|
|
||
|
|
if (!item) return undefined;
|
||
|
|
|
||
|
|
const value = item[valueField];
|
||
|
|
|
||
|
|
// 如果值是数字或可以转换为数字的字符串,则保留两位小数
|
||
|
|
if (typeof value === 'number') {
|
||
|
|
return Number(value).toFixed(2) as any;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (typeof value === 'string' && !isNaN(Number(value))) {
|
||
|
|
return Number(value).toFixed(2) as any;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 否则返回原始值
|
||
|
|
return value as any;
|
||
|
|
}
|