<template>
|
<view class="app">
|
|
|
<z-paging ref="paging" v-model="dataList" show-refresher-update-time @query="queryList">
|
<!-- 需要固定在顶部不滚动的view放在slot="top"的view中,如果需要跟着滚动,则不要设置slot="top" -->
|
<template #top>
|
<cu-custom bgColor="bg-gradual-blue" :isBack="false">
|
<block slot="content">设备列表</block>
|
</cu-custom>
|
|
|
<view class="card-box dynamic shadow">
|
<view class="title-box margin-bottom-sm">
|
<view style="width: 100vw;" class="left justify-between">
|
<view class="flex align-center">
|
<uni-text class="cuIcon-titles text-blue"></uni-text>
|
<view class="title">总览</view>
|
</view>
|
<view>
|
<text class="text-gray text-sm">{{curDate}}</text>
|
</view>
|
|
</view>
|
|
</view>
|
|
|
<!-- <view class="flex flex-direction padding-xs">
|
<view class="flex">
|
<view class="flex-sub flex flex-direction text-center">
|
<text class="text-df">设备总数</text>
|
<text class="text-bold text-sl margin-top-xs text-gray margin-top-sm">{{equCount}}
|
<text class="text-gray text-sm margin-left-xs">台</text></text>
|
</view>
|
|
<view class="flex-sub flex flex-direction text-center">
|
<text class="text-df">开机设备</text>
|
<text class="text-bold text-sl margin-top-xs text-gray margin-top-sm">{{onlineCount}}
|
<text class="text-gray text-sm margin-left-xs">台</text></text>
|
</view>
|
|
<view class="flex-sub flex flex-direction text-center">
|
<text class="text-df">运行设备</text>
|
<text class="text-bold text-sl margin-top-xs text-cyan margin-top-sm">{{onlineCount}}
|
<text class="text-gray text-sm margin-left-xs">台</text></text>
|
</view>
|
</view>
|
<view class="margin-top">
|
<u-scroll-list>
|
|
</u-scroll-list>
|
|
</view>
|
|
</view> -->
|
|
<view class="flex flex-direction padding-xs">
|
<view class="flex">
|
<view class="flex-sub flex flex-direction">
|
<text class="text-df">在线设备</text>
|
<text class="text-bold text-sl margin-top-xs text-green margin-top-sm">{{onlineCount}}
|
<text class="text-gray text-sm margin-left-xs">台</text></text>
|
|
</view>
|
<view class="flex-twice flex flex-direction justify-between">
|
<view class="flex-sub flex">
|
<view class="flex flex-direction flex-sub">
|
<text class="text-gray text-xs">设备信息</text>
|
|
<text class="text-black">设备总数:</text>
|
<text class="margin-lr-xs text-gray text-bold text-xl">{{equCount}}</text>
|
<text class="text-gray text-xs"></text>
|
|
|
</view>
|
<view class="flex flex-direction flex-sub">
|
<text class="text-gray text-xs">报警信息</text>
|
|
<text class="text-black">故障机台:</text>
|
<text class="margin-lr-xs text-red text-bold text-xl">{{faultEquCount}}</text>
|
<text class="text-gray text-xs"></text>
|
|
|
</view>
|
|
</view>
|
<view class="flex-sub flex">
|
<view class="flex flex-direction flex-sub">
|
<text class="text-white text-xs">报警信息</text>
|
<text class="text-black">停用设备:</text>
|
<text class="margin-lr-xs text-orange text-bold text-xl">0</text>
|
<text class="text-gray text-xs"></text>
|
|
|
</view>
|
<view class="flex flex-direction flex-sub">
|
<text class="text-white text-xs">机台信息</text>
|
<text class="text-black">告警机台:</text>
|
<text
|
class="margin-lr-xs text-orange text-bold text-xl">{{alarmEquCount}}</text>
|
<text class="text-gray text-xs"></text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
|
|
</view>
|
|
|
</template>
|
|
|
|
<u-skeleton rows="20" :loading="loading" :title="false">
|
<!-- 如果希望其他view跟着页面滚动,可以放在z-paging标签内 -->
|
<view class="card-item-box dynamic shadow" v-for="(item,index) in dataList" :key="index"
|
@click="itemClick(item)">
|
<view class="title-box">
|
<view class="left flex-sub">
|
<uni-text class="cuIcon-titles text-blue"></uni-text>
|
<view class="title text-cut">{{$lget(item,'name')}}</view>
|
<view class="flex title text-green text-sm">
|
<u-tag v-if="item.online" size="mini" text="在线" type="success" plain plainFill></u-tag>
|
<u-tag v-else size="mini" text="离线" type="warning" plain plainFill></u-tag>
|
<view style="width: 20rpx;height: 2rpx"></view>
|
<template v-if="$lget(item,'realData.level')!= '--'">
|
<u-tag size="mini" :text="$lget(item,'realData.level')" plain plainFill></u-tag>
|
</template>
|
|
</view>
|
</view>
|
<view class="right" style="min-width: 240rpx;">
|
<!-- <u-badge :isDot="true" type="success"></u-badge> -->
|
<!-- <view class="title text-gray text-sm">开机时间:</view> -->
|
<view class="title text-gray text-sm">{{$lget(item,'realData.workorder')}}</view>
|
</view>
|
</view>
|
|
<view class="info-box">
|
<view class="left flex-sub">
|
<view class="title text-sm">烘干药材:</view>
|
<view class="title text-sm">{{$lget(item,'realData.name')}}</view>
|
</view>
|
<view class="right" style="min-width: 240rpx;">
|
<view class="title text-sm">投料量:</view>
|
<view class="title text-sm text-gray">{{$lget(item,'realData.weight1')}}筐</view>
|
</view>
|
</view>
|
|
<view class="info-box">
|
<view class="left flex-sub">
|
<view class="title text-sm">已用时间:</view>
|
<view class="title text-sm">{{$lget(item,'realData.time3')}}min</view>
|
</view>
|
<view class="right" style="min-width: 240rpx;">
|
<view class="title text-sm">预测剩余:</view>
|
<view class="title text-sm text-gray">{{$lget(item,'realData.ai_total_time')}}min</view>
|
</view>
|
</view>
|
|
<view class="info-box">
|
<view class="left flex-sub" style="width: 60%;">
|
<view class="title text-sm text-cut">报警信息:</view>
|
<template v-if="item.errorData">
|
<view class="title text-sm text-red text-cut">{{ (item.errorData.length > 0) ? item.errorData[0]:'' }}</view>
|
</template>
|
|
<template v-else-if="item.warnData">
|
<view class="title text-sm text-yellow text-cut"> {{(item.warnData.length > 0) ? item.warnData[0]:''}}</view>
|
</template>
|
<view v-if="getErrorCount(item) > 0">
|
<u-tag size="mini" :text="getErrorCount(item)" type="error" plain></u-tag>
|
</view>
|
<view style="width: 20rpx;"></view>
|
<view v-if="getWarnCount(item) > 0">
|
<u-tag size="mini" :text="getWarnCount(item)" type="warning" plain></u-tag>
|
</view>
|
|
</view>
|
<view class="right" style="min-width: 240rpx;">
|
<view class="title text-sm">更新时间:</view>
|
<view class="title text-sm text-gray">{{ item.refreshTime}}</view>
|
</view>
|
</view>
|
|
</view>
|
|
</u-skeleton>
|
</z-paging>
|
</view>
|
</template>
|
|
<script>
|
import dayjs from 'dayjs'
|
export default {
|
data() {
|
return {
|
loading: true,
|
// v-model绑定的这个变量不要在分页请求结束中自己赋值!!!
|
dataList: [],
|
curDate: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
isProcessing: false,
|
// key->设备租户+code
|
dataMap: new Map(),
|
|
alarmEquCount: 0,
|
faultEquCount: 0,
|
timer: null,
|
mqttThrottleMs: 1000,
|
lastMqttTs: 0,
|
dateTimer: null
|
|
}
|
},
|
onShow() {
|
this.startTimer()
|
this.startDateTimer()
|
this.mqttData()
|
},
|
onHide() {
|
this.stopTimer()
|
this.stopDateTimer()
|
uni.$off(this.$constant.MQTT_TOPIC_MESSAGE)
|
},
|
onTabItemTap: function(e) {
|
getApp().globalData.selectTab = e.index
|
},
|
methods: {
|
queryList(pageNo, pageSize) {
|
this.loading = true;
|
const params = {
|
pageNo: pageNo,
|
pageSize: pageSize,
|
}
|
this.$api.queryEquList(params).then((res) => {
|
this.$refs.paging.complete(res.result.records);
|
this.loading = false
|
}).catch(res => {
|
this.$refs.paging.complete(false);
|
this.loading = false
|
})
|
|
},
|
// 设备数据和mtqq实时数据合并
|
mergeMqttRealData(mqttData) {
|
|
const {
|
tenantid,
|
machineid
|
} = mqttData;
|
if (!tenantid || !machineid) return;
|
const key = `${tenantid}_${machineid}`;
|
const targetItem = this.dataList.find(item =>
|
item.tenantId === tenantid && item.code === machineid
|
);
|
if (targetItem && !targetItem.online) {
|
mqttData = {};
|
}
|
if (targetItem) {
|
const updatedItem = {
|
...targetItem,
|
realData: mqttData
|
};
|
updatedItem.refreshTime = dayjs().format('HH:mm:ss');
|
this.$set(this.dataList, this.dataList.indexOf(targetItem), updatedItem);
|
|
}
|
|
|
},
|
|
getErrorCount(item){
|
let count = 0;
|
if(item.errorData){
|
count += item.errorData.length
|
}
|
return count;
|
},
|
getWarnCount(item){
|
let count = 0;
|
if(item.warnData){
|
count += item.warnData.length
|
}
|
return count;
|
},
|
|
startTimer() {
|
this.timer = setInterval(() => {
|
//this.queryRealFaultData();
|
}, 3000);
|
},
|
stopTimer() {
|
clearInterval(this.timer);
|
},
|
startDateTimer() {
|
this.dateTimer = setInterval(() => {
|
this.curDate = dayjs().format('YYYY-MM-DD HH:mm:ss')
|
}, 1000);
|
},
|
stopDateTimer() {
|
clearInterval(this.dateTimer);
|
},
|
queryRealFaultData() {
|
//发送数据
|
const message = {
|
req: this.deviceId,
|
tenantId: this.tenantId,
|
timeStamp: new Date(),
|
|
}
|
let opts = {
|
topic: this.$constant.MOBILE_REQ_EQU_REAL_FAULT,
|
message: JSON.stringify(message),
|
}
|
|
this.$mqttTool.publish(opts).then(res => {
|
|
})
|
},
|
|
itemClick(item) {
|
uni.navigateTo({
|
url: "/packageA/pages/device/control?code=" + item.code + "&name=" + item.name + "&clientId=" +
|
item.clientId
|
})
|
},
|
mqttData() {
|
uni.$on(this.$constant.MQTT_TOPIC_MESSAGE, (data) => {
|
try {
|
const now = Date.now();
|
if (now - this.lastMqttTs < this.mqttThrottleMs) return;
|
this.lastMqttTs = now;
|
// 1. 数据解析和验证
|
const {
|
data: wdata,
|
topic
|
} = this.parseAndValidateMqttData(data);
|
if (!wdata || !topic) return;
|
|
// 2. 准备主题常量
|
const topics = this.prepareMqttTopics();
|
|
// 3. 更新刷新时间
|
this.refreshTime = dayjs().format('HH:mm:ss');
|
|
// 4. 根据主题处理数据
|
switch (topic) {
|
case topics.updateEquStatu:
|
this.handleEquipmentStatusUpdate(wdata);
|
break;
|
case topics.realData:
|
this.mergeMqttRealData(wdata);
|
break;
|
case topics.realFaultTopic:
|
case topics.oneceFaultTopic:
|
this.handleFaultData(wdata);
|
break;
|
}
|
} catch (error) {
|
console.error('MQTT数据处理错误:', error);
|
}
|
});
|
},
|
|
// 辅助方法:解析和验证MQTT数据
|
parseAndValidateMqttData(data) {
|
try {
|
const json = JSON.parse(data);
|
if (!json?.data || !json?.topic) return {
|
data: null,
|
topic: null
|
};
|
return {
|
data: json.data,
|
topic: json.topic
|
};
|
} catch (e) {
|
return {
|
data: null,
|
topic: null
|
};
|
}
|
},
|
|
// 辅助方法:准备MQTT主题
|
prepareMqttTopics() {
|
return {
|
updateEquStatu: this.$constant.SERVICE_BROADCAST_TENANT_UPDATE_EQU_STATU
|
.replace('%s', this.tenantId),
|
realData: this.$constant.SERVICE_BROADCAST_TENANT_REAL_DATA
|
.replace('%s', this.tenantId),
|
realFaultTopic: this.$constant.SERVICE_BROADCAST_TENANT_REAL_FAULT
|
.replace('%s', this.tenantId),
|
oneceFaultTopic: this.$constant.SERVICE_ONECE_TENANT_REAL_FAULT.replace('%s', this
|
.deviceId)
|
};
|
},
|
|
// 辅助方法:处理设备状态更新
|
handleEquipmentStatusUpdate(wdata) {
|
const {
|
connected,
|
tenantId,
|
code
|
} = wdata;
|
const index = this.dataList.findIndex(item =>
|
(item.tenantId + "") === tenantId && item.code === code
|
);
|
|
if (index === -1) return;
|
|
// 更新在线状态
|
this.$set(this.dataList[index], 'online', connected);
|
|
// 如果离线,删除实时数据
|
if (!connected && this.dataList[index]?.realData) {
|
this.$delete(this.dataList[index], 'realData');
|
}
|
},
|
|
// 辅助方法:处理故障数据
|
handleFaultData(wdata) {
|
if (!Array.isArray(wdata)) return;
|
|
// 按设备分组故障数据
|
const faultMap = wdata.reduce((map, item) => {
|
const key = `${item.tenantId}_${item.equCode}`;
|
map.get(key)?.push(item) || map.set(key, [item]);
|
return map;
|
}, new Map());
|
|
// 更新每个设备的故障信息
|
faultMap.forEach((faults, key) => {
|
const [tenantId, equCode] = key.split("_");
|
const targetItem = this.dataList.find(i =>
|
i.tenantId == tenantId && i.code == equCode
|
);
|
|
if (targetItem) {
|
const updatedItem = {
|
...targetItem,
|
errorData: targetItem.online ? faults.filter(f => f.faultType === 1).map(n => n.faultName) : [],
|
warnData: targetItem.online ? faults.filter(f => f.faultType === 2).map(n => n.faultName) : [],
|
};
|
if(targetItem.online){
|
updatedItem.refreshTime = dayjs().format('HH:mm:ss');
|
}
|
|
this.$set(this.dataList, this.dataList.indexOf(targetItem), updatedItem);
|
}
|
});
|
|
this.faultEquCount = this.dataList.filter(item => {
|
return (
|
item.errorData &&
|
Array.isArray(item.errorData) &&
|
item.errorData.length > 0
|
);
|
}).length || 0;
|
|
this.alarmEquCount = this.dataList.filter(item => {
|
return (
|
item.warnData &&
|
Array.isArray(item.warnData) &&
|
item.warnData.length > 0
|
);
|
}).length || 0;
|
|
}
|
|
|
|
},
|
onReady() {
|
|
},
|
computed: {
|
equCount() {
|
return this.dataList.length
|
},
|
onlineCount() {
|
const list = this.dataList.filter(item => item.online)
|
return list.length
|
},
|
tenantId() {
|
const userinfo = uni.getStorageSync('userinfo');
|
const tenantid = userinfo.loginTenantId
|
return tenantid + "";
|
},
|
deviceId() {
|
return uni.getStorageSync(this.$constant.DEVICE_ID);
|
}
|
}
|
}
|
</script>
|
|
<style lang="scss" scoped>
|
.app {
|
width: 100%;
|
max-height: 100vh;
|
overflow: hidden;
|
}
|
|
.card-box {
|
margin: 20rpx;
|
padding: 20rpx;
|
box-sizing: border-box;
|
background-color: white;
|
border-radius: 20rpx;
|
font-family: Helvetica Neue, Helvetica, sans-serif;
|
}
|
|
.card-item-box {
|
margin: 0rpx 20rpx 10rpx 20rpx;
|
padding: 20rpx;
|
box-sizing: border-box;
|
background-color: white;
|
border-radius: 20rpx;
|
font-family: Helvetica Neue, Helvetica, sans-serif;
|
}
|
|
/* 第二个及之后的 item */
|
.card-item-box:not(:first-child) {
|
margin: 10rpx 20rpx;
|
}
|
|
.margin-left-sm {
|
margin-left: 20rpx;
|
}
|
|
|
.title-box {
|
display: flex;
|
flex-direction: row;
|
align-items: center;
|
|
.left {
|
display: flex;
|
align-items: center;
|
|
.title {
|
margin: 0 10rpx;
|
font-weight: bold;
|
}
|
}
|
|
.right {
|
display: flex;
|
align-items: center;
|
|
.title {
|
margin: 0 10rpx;
|
font-weight: bold;
|
}
|
}
|
}
|
|
.info-box {
|
margin-top: 20rpx;
|
display: flex;
|
flex-direction: row;
|
align-items: center;
|
|
.left {
|
display: flex;
|
align-items: center;
|
|
.title {
|
margin: 0 10rpx;
|
}
|
}
|
|
.right {
|
display: flex;
|
align-items: center;
|
|
.title {
|
margin: 0 10rpx;
|
}
|
}
|
}
|
</style>
|