<script setup>
|
import { ref, computed, watch, onMounted, onActivated } from "vue";
|
import ycCard from "@/components/ycCard.vue";
|
import useWebSocket from "@/hooks/useWebSocket.js";
|
import bar from "@/components/echarts/bar2.vue";
|
import iconPower from "@/components/icons/iconPower.vue";
|
import formatSeconds from "@/assets/js/tools/formatSeconds.js";
|
import getPowerByUI from "@/assets/js/tools/getPowerByUI.js";
|
import toFixed from "@/assets/js/tools/toFixed.js";
|
import const_digits from "@/assets/js/const/const_digits";
|
import IconAvg from "@/components/icons/IconAvg.vue";
|
const { VOL, GROUPVOL, CURR_YT, CURR_JH } = const_digits;
|
|
const infoTab = ref(0);
|
const { sendData, message } = useWebSocket("rtstateSocket");
|
|
const props = defineProps({
|
devId: {
|
type: [Number, String],
|
required: true,
|
},
|
});
|
|
const battVolChart = ref();
|
|
const rtDatas = computed(() => {
|
if (message.value) {
|
const {
|
code,
|
data2: {
|
rtdataState: { data2: monStates },
|
resA200State,
|
event: { data2: eventList },
|
},
|
} = JSON.parse(message.value);
|
|
let devStates = resA200State?.data2 || {};
|
|
return { monStates, devStates, eventList };
|
} else {
|
return { monStates: [], devStates: {}, eventList: [] };
|
}
|
});
|
|
const logList = computed(() => {
|
let _list = rtDatas.value.eventList;
|
let resObj = {};
|
_list.forEach((v) => {
|
let { num, devId, battIdx, lastWorkState, nowWorkState, recordTime } = v;
|
let [date, time] = recordTime.split(" ");
|
resObj[date] = resObj[date] || { date, list: [] };
|
v.time = time;
|
resObj[date].list.push(v);
|
});
|
return Object.keys(resObj).map((v) => resObj[v]);
|
});
|
|
watch(
|
() => props.devId,
|
(v) => {
|
let reg = /^1/;
|
if (reg.test(v)) {
|
sendData(JSON.stringify({ devId: props.devId, devType: 1 }));
|
}
|
},
|
{ immediate: true }
|
);
|
|
watch(message, (val) => {
|
changeTab();
|
});
|
|
function changeTab() {
|
if (infoTab.value == 2) {
|
updateChart();
|
}
|
}
|
function updateChart() {
|
let mons = [],
|
vols = [];
|
rtDatas.value.monStates.forEach((v) => {
|
mons.push(`#${v.monNum}`);
|
vols.push(toFixed(v.monVol, VOL));
|
});
|
battVolChart.value?.updateChart(mons, vols);
|
}
|
|
onActivated(() => {
|
});
|
|
onMounted(() => {
|
let reg = /^1/;
|
if (reg.test(props.devId)) {
|
sendData(JSON.stringify({ devId: props.devId, devType: 1 }));
|
}
|
// sendData(JSON.stringify({ devId: props.devId, devType: 1 }));
|
});
|
</script>
|
|
<template>
|
<yc-card class="p-info" title="实时监测">
|
<template #tools>
|
<el-radio-group
|
v-model="infoTab"
|
@change="changeTab"
|
size="small"
|
is-button
|
>
|
<el-radio-button :value="0">测试信息</el-radio-button>
|
<el-radio-button :value="1">单体列表</el-radio-button>
|
<el-radio-button :value="2">单体电压图</el-radio-button>
|
<el-radio-button :value="3">状态日志</el-radio-button>
|
</el-radio-group>
|
</template>
|
<div class="tab-container">
|
<transition-group :duration="300" name="slide-left">
|
<div class="tab-content test-content" v-if="infoTab == 0">
|
<div class="state" v-if="rtDatas.devStates.isTesting">
|
<div
|
:class="[
|
'item',
|
{
|
active:
|
rtDatas.devStates.isTesting &&
|
2 == rtDatas.devStates.testType,
|
},
|
]"
|
>
|
充电测试
|
</div>
|
<div
|
:class="[
|
'item',
|
{
|
active:
|
rtDatas.devStates.isTesting &&
|
1 == rtDatas.devStates.testType,
|
},
|
]"
|
>
|
放电测试
|
</div>
|
</div>
|
<div class="stop-reason" v-else>
|
<span class="label">停止原因:</span
|
>{{ rtDatas.devStates.stopReason }}
|
</div>
|
<div class="content">
|
<div class="item item1">
|
<div class="label">Umax</div>
|
<div class="value">
|
{{ toFixed(rtDatas.devStates.maxBatteryVoltage, VOL) }}V
|
</div>
|
</div>
|
<div class="item">
|
<div class="label">Umin</div>
|
<div class="value">
|
{{ toFixed(rtDatas.devStates.minBatteryVoltage, VOL) }}V
|
</div>
|
</div>
|
<div class="item">
|
<div class="label">
|
<el-icon><icon-avg class="avg-icon" /></el-icon>U
|
</div>
|
<div class="value">
|
{{ toFixed(rtDatas.devStates.avgMonVol, VOL) }}V
|
</div>
|
</div>
|
<div class="item-big">
|
{{ toFixed(rtDatas.devStates.storageVoltage, GROUPVOL) }}
|
<div class="unit">V</div>
|
</div>
|
|
<div class="border">
|
<div class="i">
|
<el-icon><Clock /></el-icon>
|
</div>
|
<div class="value">
|
{{ formatSeconds(rtDatas.devStates.testDuration) }}
|
</div>
|
</div>
|
|
<div class="center">
|
<div class="i"></div>
|
<div class="value">
|
{{ rtDatas.devStates.testCapacity }}
|
<div class="unit">AH</div>
|
</div>
|
</div>
|
<div class="border border-center">
|
<div class="i label">电芯压差</div>
|
<div class="value">
|
{{ rtDatas.devStates.diffBatteryVoltage }} mV
|
</div>
|
</div>
|
|
<div class="item-big curr">
|
{{ toFixed(rtDatas.devStates.testCurrent, CURR_YT) }}
|
<div class="unit">A</div>
|
</div>
|
<div class="border border2">
|
<div class="value">
|
<!-- TODO -->
|
{{
|
getPowerByUI(
|
rtDatas.devStates.storageVoltage,
|
rtDatas.devStates.testCurrent
|
)
|
}}KW
|
</div>
|
<div class="i">
|
<el-icon><icon-power /></el-icon>
|
</div>
|
</div>
|
|
<div class="item item1">
|
<div class="label">Tmax</div>
|
<div class="value">
|
{{ rtDatas.devStates.maxBatteryTemperature }}℃
|
</div>
|
</div>
|
<div class="item">
|
<div class="label">Tmin</div>
|
<div class="value">
|
{{ rtDatas.devStates.minBatteryTemperature }}℃
|
</div>
|
</div>
|
<div class="item">
|
<div class="label">
|
<el-icon><icon-avg class="avg-icon" /></el-icon>T
|
</div>
|
<div class="value">{{ rtDatas.devStates.avgMonTmp }}℃</div>
|
</div>
|
</div>
|
</div>
|
<!-- 单体列表 -->
|
<div class="tab-content tab-batt" v-if="infoTab == 1">
|
<div class="info">
|
<div class="label">最大值</div>
|
<div class="value max">
|
{{ toFixed(rtDatas.devStates.maxBatteryVoltage, VOL) }}V
|
</div>
|
<div class="label">最小值</div>
|
<div class="value min">
|
{{ toFixed(rtDatas.devStates.minBatteryVoltage, VOL) }}V
|
</div>
|
<div class="label">平均值</div>
|
<div class="value">
|
{{ toFixed(rtDatas.devStates.avgMonVol, VOL) }}V
|
</div>
|
</div>
|
<div class="list-wrap posR">
|
<div class="pos-full scroll">
|
<div
|
class="item"
|
v-for="item in rtDatas.monStates"
|
:key="'test_' + item.monNum"
|
>
|
<div class="label">#{{ item.monNum }}</div>
|
<!-- TODO -->
|
<div
|
:class="[
|
'value',
|
{
|
max: rtDatas.devStates.maxBatteryVoltage == item.monVol,
|
min: rtDatas.devStates.minBatteryVoltage == item.monVol,
|
},
|
]"
|
>
|
{{ toFixed(item.monVol, VOL) }} V
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
<div class="tab-content tab-chart" v-if="infoTab == 2">
|
<div class="chart-info">
|
<div class="label">最大值</div>
|
<div class="value max">
|
{{ toFixed(rtDatas.devStates.maxBatteryVoltage, VOL) }}V
|
</div>
|
<div class="label">最小值</div>
|
<div class="value min">
|
{{ toFixed(rtDatas.devStates.minBatteryVoltage, VOL) }}V
|
</div>
|
<div class="label">平均值</div>
|
<div class="value">
|
{{ toFixed(rtDatas.devStates.avgMonVol, VOL) }}V
|
</div>
|
</div>
|
<div class="chart-wrap">
|
<bar ref="battVolChart" unit="V"></bar>
|
</div>
|
</div>
|
<div class="tab-content scroll" v-if="infoTab == 3">
|
<el-timeline class="custom-timeline time-left" v-if="logList.length">
|
<el-timeline-item
|
v-for="item in logList"
|
:timestamp="item.date"
|
placement="top"
|
:key="item.date"
|
>
|
<div class="card">
|
<div class="row" v-for="row in item.list" :key="row.num">
|
<div class="time">{{ row.time }}</div>
|
<div class="content">{{ row.eventStr }}</div>
|
</div>
|
</div>
|
</el-timeline-item>
|
</el-timeline>
|
<el-empty v-else description="暂无记录" />
|
</div>
|
</transition-group>
|
</div>
|
</yc-card>
|
</template>
|
|
<style scoped lang="less">
|
.custom-timeline :deep(.el-timeline-item__tail) {
|
// left: auto;
|
// right: 18px;
|
}
|
|
.custom-timeline :deep(.el-timeline-item__content) {
|
margin-left: 0;
|
margin-right: 30px;
|
}
|
|
.custom-timeline :deep(.el-timeline-item__timestamp) {
|
position: absolute;
|
left: 0;
|
transform: translateX(-100%);
|
padding-right: 20px;
|
text-align: right;
|
color: #fff;
|
}
|
.custom-timeline {
|
.card {
|
color: #fff;
|
padding-top: 10px;
|
.row {
|
display: flex;
|
height: 38px;
|
align-items: center;
|
background: rgba(0, 0, 0, 0.2);
|
border-radius: 6px;
|
margin-top: 6px;
|
padding-left: 2em;
|
.time {
|
margin-right: 0.8em;
|
color: #0ff;
|
font-weight: bold;
|
}
|
}
|
}
|
&.time-left {
|
padding-left: 160px;
|
}
|
}
|
.p-info {
|
.tab-chart {
|
height: 100%;
|
display: flex;
|
flex-direction: column;
|
.chart-info {
|
display: grid;
|
grid-template-columns: repeat(3, 4em 6em);
|
place-content: center center;
|
// place-items: center center;
|
gap: 6px;
|
.label {
|
display: flex;
|
justify-content: end;
|
align-items: center;
|
&::after {
|
content: ":";
|
}
|
}
|
.value {
|
display: flex;
|
justify-content: center;
|
align-items: center;
|
height: 36px;
|
text-align: center;
|
background: #02a7f0;
|
&.max {
|
background: #438d29;
|
}
|
&.min {
|
background: #dbd608;
|
}
|
}
|
}
|
.chart-wrap {
|
flex: 1;
|
// background: #000;
|
}
|
}
|
.tab-batt {
|
display: flex;
|
flex-direction: column;
|
.info {
|
display: grid;
|
grid-template-columns: repeat(3, 4em 6em);
|
place-content: center center;
|
// place-items: center center;
|
gap: 6px;
|
.label {
|
display: flex;
|
justify-content: end;
|
align-items: center;
|
&::after {
|
content: ":";
|
}
|
}
|
.value {
|
display: flex;
|
justify-content: center;
|
align-items: center;
|
height: 36px;
|
text-align: center;
|
background: #02a7f0;
|
&.max {
|
background: #438d29;
|
}
|
&.min {
|
background: #dbd608;
|
}
|
}
|
}
|
.list-wrap {
|
margin-top: 8px;
|
flex: 1;
|
.scroll {
|
// min-height: 0;
|
display: grid;
|
place-content: start center;
|
grid-template-columns: repeat(auto-fill, 200px);
|
overflow-y: auto;
|
gap: 24px;
|
.item {
|
display: flex;
|
border-radius: 6px;
|
background: #1e6866;
|
height: 36px;
|
align-items: center;
|
padding: 0 0.4em 0 1em;
|
.label {
|
margin-right: 1em;
|
width: 3em;
|
text-align: right;
|
}
|
.value {
|
background: #02a7f0;
|
flex: 1;
|
padding: 2px 10px;
|
border-radius: 6px;
|
&.max {
|
background: #438d29;
|
}
|
&.min {
|
background: #dbd608;
|
}
|
}
|
}
|
}
|
}
|
}
|
.test-content {
|
// background: #000;
|
display: flex;
|
flex-direction: column;
|
.state {
|
display: flex;
|
justify-content: center;
|
align-items: flex-end;
|
margin-bottom: 6px;
|
height: 40px;
|
.item {
|
color: #ccc;
|
border-bottom: 1px #81d3f8 solid;
|
padding: 0 1em;
|
&.active {
|
border-bottom: 4px #fff solid;
|
font-size: 22px;
|
color: #fff;
|
}
|
}
|
}
|
.stop-reason {
|
// margin-left: 4em;
|
text-align: center;
|
/* 定义动画名称和持续时间 */
|
animation: colorChange 3s infinite;
|
font-weight: bold;
|
.label {
|
margin-right: 0.4em;
|
color: #ccc;
|
font-weight: normal;
|
}
|
}
|
.content {
|
flex: 1;
|
// background: #000;
|
display: grid;
|
grid-template-columns: 1fr 2.3fr 140px 2.3fr 1fr;
|
grid-template-rows: repeat(5, 1fr);
|
gap: 10px 20px;
|
grid-auto-flow: column;
|
place-items: center;
|
padding: 30px;
|
&::before {
|
content: "";
|
grid-area: 5 e("/") 1 e("/") 6 e("/") 2;
|
}
|
.item {
|
place-self: center stretch;
|
display: flex;
|
border-radius: 6px;
|
background: #1e6866;
|
height: 36px;
|
align-items: center;
|
padding: 0 0.4em 0 1em;
|
.label {
|
margin-right: 1em;
|
width: 3em;
|
text-align: right;
|
}
|
.value {
|
background: #02a7f0;
|
flex: 1;
|
padding: 2px 10px;
|
border-radius: 6px;
|
}
|
&.item1 {
|
grid-row: 2 e("/") 3;
|
}
|
}
|
.item-big {
|
grid-row: 1 e("/") 5;
|
grid-column: 2 e("/") 3;
|
place-self: stretch;
|
display: flex;
|
justify-content: center;
|
align-items: center;
|
font-size: 60px;
|
font-weight: bold;
|
position: relative;
|
&::before {
|
content: "";
|
position: absolute;
|
left: 0;
|
right: 0;
|
top: 0;
|
bottom: 0;
|
background: url('data:image/svg+xml,%3csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 300"%3e%3cpath d="M200,4h-80l-80,130l80,130h70" stroke="%2302a7f0" fill="none" stroke-width="2" %3e%3c/path%3e%3cpath d="M60,66l-40,65l92,150h110" stroke="%230ff" fill="none" stroke-width="10" %3e%3c/path%3e%3c/svg%3e')
|
center center / contain no-repeat;
|
}
|
.unit {
|
color: #ccc;
|
font-size: 16px;
|
margin-left: 0.4em;
|
margin-top: 2em;
|
}
|
&.curr {
|
grid-column: 4 e("/") 5;
|
&::before {
|
transform: scaleX(-1);
|
}
|
}
|
}
|
.border {
|
grid-column: 2 e("/") 3;
|
border: 1px #0ff solid;
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
padding: 0 0.4em;
|
height: 40px;
|
font-size: 24px;
|
width: 8em;
|
&.border2 {
|
grid-column: 4 e("/") 5;
|
}
|
&.border-center {
|
grid-column: 3 e("/") 4;
|
width: 9em;
|
}
|
.i {
|
margin-top: 0.2em;
|
&.label {
|
font-size: 16px;
|
}
|
}
|
}
|
.center {
|
display: flex;
|
flex-direction: column;
|
justify-content: center;
|
align-items: center;
|
place-self: stretch;
|
grid-area: 1 e("/") 3 e("/") 5 e("/") 4;
|
.i {
|
height: 150px;
|
width: 100%;
|
background: url('data:image/svg+xml,%3csvg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"%3e%3cpath d="M630.997333 916.074667l57.728 0.021333h36.48l6.698667 0.021333h18.794667l5.802666 0.021334h15.893334l4.757333 0.021333h12.586667l3.562666 0.021333h6.250667l2.624 0.021334h4.245333l1.578667 0.021333 2.090667 0.021333c0.874667 0 0.768 0.021333-0.448 0.042667A65.066667 65.066667 0 0 1 744.576 981.333333H279.424a65.066667 65.066667 0 0 1-64.96-61.226666l-0.106667-3.84c-1.216 0-1.344 0-0.426666-0.021334h2.069333l1.578667-0.021333h4.245333l2.624-0.021333h6.229333l3.605334-0.021334h12.565333l4.757333-0.021333 15.893334-0.021333h24.597333l6.698667-0.021334h28.8l7.68-0.021333h57.728l8.746666-0.021333h229.248z m-100.245333-698.773334l37.269333 0.042667c133.12 0.149333 250.816 0.853333 242.197334 2.133333l-0.576 0.064v672.917334c12.992 1.28-99.050667 2.005333-229.418667 2.197333l-14.293333 0.021333h-7.168l-18.645334 0.021334H465.237333l-7.189333-0.021334h-14.293333c-128-0.213333-238.314667-0.917333-229.973334-2.133333l0.576-0.085333V219.541333c-13.397333-1.322667 106.133333-2.048 241.621334-2.197333l37.248-0.042667h37.546666zM640 348.288l-250.282667 190.570667 85.674667 23.402666L384 741.248l250.282667-190.570667-85.653334-23.402666L640 348.288z m104.576-218.304a65.066667 65.066667 0 0 1 65.066667 65.066667c1.216 0 1.322667 0 0.426666 0.021333h-2.069333l-1.578667 0.021333h-4.245333l-2.624 0.021334h-6.250667l-3.584 0.021333-12.586666 0.021333h-4.736l-15.893334 0.021334h-24.597333l-6.72 0.021333h-28.8l-7.658667 0.021333-57.728 0.021334H392.981333l-57.728-0.021334H298.816l-6.72-0.021333h-18.794667l-5.802666-0.021333h-15.893334l-4.757333-0.021334h-12.586667l-3.584-0.021333h-6.229333l-2.624-0.021333h-4.245333l-1.578667-0.021334-2.090667-0.021333c-0.896 0-0.768-0.021333 0.448-0.042667l0.106667-3.818666a65.066667 65.066667 0 0 1 64.96-61.226667zM554.453333 42.666667a21.333333 21.333333 0 0 1 21.333334 21.333333v43.946667c1.92 0.085333-9.130667 0.128-25.472 0.170666h-9.386667l-4.992 0.021334h-62.250667a843.136 843.136 0 0 1-25.472-0.213334V64a21.333333 21.333333 0 0 1 21.333334-21.333333z" fill="%231296db"%3e%3c/path%3e%3c/svg%3e')
|
center center / auto 80% no-repeat;
|
}
|
.value {
|
display: flex;
|
align-items: center;
|
font-size: 32px;
|
font-weight: bold;
|
.unit {
|
color: #ccc;
|
font-size: 14px;
|
margin-left: 0.2em;
|
margin-top: 1em;
|
}
|
}
|
}
|
}
|
}
|
}
|
.tab-container {
|
height: 100%;
|
position: relative;
|
overflow: hidden;
|
.tab-content {
|
position: absolute;
|
left: 0;
|
top: 0;
|
right: 0;
|
bottom: 0;
|
height: 100%;
|
color: #fff;
|
padding: 6px;
|
&.scroll {
|
right: 6px;
|
bottom: 4px;
|
overflow-y: auto;
|
}
|
}
|
|
.slide-left-enter-active,
|
.slide-left-leave-active,
|
.slide-right-enter-active,
|
.slide-right-leave-active {
|
transition: all 0.3s;
|
}
|
|
.slide-left-enter-from {
|
transform: translateX(100%);
|
}
|
|
.slide-left-leave-to {
|
transform: translateX(-100%);
|
}
|
|
.slide-right-enter-from {
|
transform: translateX(-100%);
|
}
|
|
.slide-right-leave-to {
|
transform: translateX(100%);
|
}
|
|
.slide-left-enter-to,
|
.slide-right-enter-to {
|
transform: translateX(0);
|
}
|
}
|
|
/* 使用@keyframes定义动画 */
|
@keyframes colorChange {
|
0%,
|
100% {
|
color: red;
|
} /* 开始和结束时的颜色 */
|
50% {
|
color: #0ff;
|
} /* 中间状态的颜色 */
|
}
|
</style>
|