2025-05-22 16:23:08 +08:00

727 lines
20 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="status-variable">
<view class="nav-bar">
<view v-if="!isSearch" style="margin-right: 20rpx;">
<u-icon name="search" size="27" @click="isSearch = true"></u-icon>
</view>
<view v-else style="width: 100%;">
<!-- #ifndef APP-NVUE -->
<u-input :customStyle="{ padding: '17rpx 36rpx', background: '#FFFFFF' }"
v-model="queryParams.modelName" :placeholder="$tt('group.inputContent')" shape="circle"
@clear="handleClearSearch" clearable>
<!-- #endif -->
<!-- #ifdef APP-NVUE -->
<u--input :customStyle="{ padding: '17rpx 36rpx', background: '#FFFFFF' }"
v-model="queryParams.modelName" :placeholder="$tt('group.inputContent')" shape="circle"
@clear="handleClearSearch" clearable>
<!-- #endif -->
<template slot="prefix">
<u-icon name="search" color="rgb(192, 196, 204)" size="26"
@click="isSearch = false"></u-icon>
</template>
<template slot="suffix">
<view style="display: flex; flex-direction: row; align-items: center;">
<span style="width: 0px; height: 14px; border: 1px solid #000000; opacity: 0.1;"></span>
<span style="font-size: 14px; font-weight: 400; color: #3378FE; margin-left: 24rpx;"
@click="handleSearch">{{$tt('common.search')}}</span>
</view>
</template>
<!-- #ifndef APP-NVUE -->
</u-input>
<!-- #endif -->
<!-- #ifdef APP-NVUE -->
</u--input>
<!-- #endif -->
</view>
<view class="right-wrap"
v-if="device.protocolCode === 'MODBUS-TCP' || device.protocolCode === 'MODBUS-RTU'">
<u-icon name="/static/common/refresh_blue.png" color="#3378FE" size="28"
@click="handleCollectionAll"></u-icon>
</view>
</view>
<view class="event-wrap">
<view class="item" v-for="(item, index) in datas" :key="index">
<view class="status"
:style="{ background: item.type === 1 ? '#486ff2' : item.type === 2 ? '#13ce66' : '#ffba00' }">
<text v-if="item.type === 3">{{$tt('variable.041-1')}}</text>
<text v-if="item.type === 1">{{$tt('variable.041-2')}}</text>
<text v-if="item.type === 2">{{$tt('variable.041-3')}}</text>
</view>
<view class="left">
<view class="name">{{item.modelName}}</view>
<view class="time">{{item.ts || '--'}}</view>
</view>
<view class="right">
<view class="data">
<text v-if="item.dataTypeName === 'bool' || item.dataTypeName === 'enum'">
{{item.valueName || '--'}}
</text>
<text v-else>
{{item.value || '--'}}
</text>
<text v-if="!!item.unit" style="margin-left: 5px;">{{item.unit}}</text>
</view>
<view class="icon">
<u-icon v-if="device.protocolCode === 'MODBUS-TCP' || device.protocolCode === 'MODBUS-RTU'"
name="/static/common/refresh.png" size="22" @click="handleActiveCollect(item)"></u-icon>
<u-icon v-if="item.isReadonly === 0" name="edit-pen" size="22"
@click="handleIssueInstruction(item)"></u-icon>
<!-- 站位符 -->
<view v-else style="width: 22px; height: 22px;"></view>
</view>
</view>
</view>
<u-loadmore v-if="total > queryParams.pageSize" :status="loadmoreStatus"
:loading-text="$tt('variable.041-5')" :loadmoreText="$tt('variable.041-6')"
:nomoreText="$tt('variable.041-7')" marginTop="20" @loadmore="loadMoreLogs" />
<u-empty mode="data" :show="total === 0" marginTop="60" :text="$tt('scene.emptyData')"></u-empty>
</view>
<!-- 下发数据弹窗 -->
<view class="other">
<!-- 数据采集提示框 -->
<u-modal :show="isCollect" :content="$tt('variable.041-4')" @confirm="handleCollectConfirm"
@cancel="isCollect = false" showCancelButton></u-modal>
<!-- 下发数据 -->
<u-popup :show="isIssue" :round="5" mode="bottom" bgColor="#eef3f7" @close="isIssue = false">
<view class="issue-popup">
<view class="nav">
<text @click="isIssue = false">{{$tt('common.cancel')}}</text>
<text @click="handleSendService">{{$tt('common.confirm')}}</text>
</view>
<u--form labelPosition="left" label-width="100px">
<u-form-item v-for="(item, index) in issueOpations" :label="`${item.label}`" :key="index">
<!-- 字符串 -->
<view style="width: 100%;"
v-if="item.dataTypeName === 'string'||(item.dataTypeName === 'array' && item.arrayType === 'string')">
<u--input :placeholder="$tt('common.PleaseIpt')" border="none" inputAlign="right"
v-model="funVal[item.key]"></u--input>
</view>
<!-- 整数,小数 -->
<view class="with-range"
v-if="item.dataTypeName == 'integer' || item.dataTypeName == 'decimal'||item.dataTypeName == 'array'||(item.dataTypeName == 'array'&&item.arrayType=='integer')||(item.dataTypeName == 'array'&&item.arrayType=='decimal')">
<u--input v-model="funVal[item.key]" inputAlign="right"
:placeholder="$tt('common.PleaseIpt')" @input="justNumber(item, $event)"
type="number" border="none"></u--input>
<text v-if="item.unit && item.unit != 'un' && item.unit != '/'">
{{item.unit}}
</text>
<text> ({{ item.min }} ~ {{ item.max }})</text>
</view>
<!-- 枚举,布尔型 -->
<view style="width: 100%;"
v-if="item.dataTypeName === 'enum' || item.dataTypeName === 'bool'"
@click="handleOpenModel">
<u--input v-model="models.label" disabled disabledColor="#ffffff"
:placeholder="$tt('common.PleaseSelect')" border="none"
suffixIcon="arrow-right"></u--input>
</view>
<!--枚举,布尔型,弹出选择框 -->
<u-popup :show="isShowModel" :round="5" mode="bottom" @close="isShowModel = false">
<view class="model-popup">
<view class="cell-group-wrap">
<u-cell-group :border="false">
<view class="cell-wrap" v-for="(item1,index1) in item.options"
:key="index1">
<u-cell :title="item1.label" :name="item1.value" :border="false"
@click="handleConfirmOperator(item1)"></u-cell>
</view>
<view class="cell-wrap">
<u-cell :title="$tt('common.cancel')" name="cancel" :border="false"
@click="isShowModel = false"></u-cell>
</view>
</u-cell-group>
</view>
</view>
</u-popup>
</u-form-item>
</u--form>
</view>
</u-popup>
<u-loading-page :loading="loading" bg-color="#eef3f7" loadingText="FastBee.cn"></u-loading-page>
</view>
</view>
</template>
<script>
import {
listThingsModel,
getOrderControl,
propGet,
serviceInvokeReply,
serviceInvoke,
} from '@/apis/modules/device.js';
export default {
name: "DeviceVariable",
options: {
styleIsolation: 'shared'
},
props: {
device: {
type: Object,
default: null,
required: true
}
},
watch: {
// 兼容小程序
device: function(newVal, oldVal) {
this.deviceInfo = newVal;
if (newVal.deviceId && newVal.deviceId !== oldVal.deviceId) {
this.queryParams.deviceId = newVal.deviceId;
this.getVariableList();
}
},
},
data() {
return {
isSearch: true,
loading: false,
queryParams: {
deviceId: '',
pageNum: 1,
pageSize: 10,
},
datas: [],
loadmoreStatus: 'loadmore', // 加载更多
total: 0, // 总条数
isCollect: false, // 是否开始采集
collectParams: {}, // 采集参数
isIssue: false, // 下发弹窗
issueOpations: [], // 下发数据
chooseFun: {}, // 选中项
funVal: {}, // 数据对象
isCanSend: true, // 是否可以下发,主要判断数值在不在范围
models: {}, // 下发数据弹框类型存储选择数据
isShowModel: false,
serialNumber: '', // 下发的设备
deviceInfo: {},
};
},
mounted() {
const { deviceId, serialNumber } = this.device;
if (deviceId) {
this.queryParams.deviceId = deviceId;
this.serialNumber = serialNumber;
this.getVariableList();
}
},
created() {
// 回调处理
this.mqttCallback();
},
methods: {
/* Mqtt回调处理 */
mqttCallback() {
this.$mqttTool.client.on('message', (topic, message, buffer) => {
let topics = topic.split('/');
let productId = topics[1];
let deviceNum = topics[2];
message = JSON.parse(message.toString());
if (topics[3] == 'status') {
console.log('接收到【设备状态-运行】主题:', topic);
console.log('接收到【设备状态-运行】内容:', message);
// 更新列表中设备的状态
if (this.deviceInfo.serialNumber == deviceNum) {
this.deviceInfo.status = message.status;
this.deviceInfo.isShadow = message.isShadow;
this.deviceInfo.rssi = message.rssi;
}
}
//兼容设备回复
if (topics[4] == 'reply') {
uni.showToast({
icon: 'none',
title: message,
})
}
if (topics[3] == 'property' || topics[3] == 'function' || topic.endsWith(
'ws/service')) {
console.log('接收到【物模型】主题:', topic);
console.log('接收到【物模型】内容:', message);
// 更新列表中设备的属性
if (this.deviceInfo.serialNumber == deviceNum) {
for (let j = 0; j < message.message.length; j++) {
let isComplete = false;
// 设备状态
for (let k = 0; k < this.datas.length && !
isComplete; k++) {
if (this.datas[k].identifier == message.message[j].id) {
// 普通类型
this.datas[k].value = message.message[j].value;
this.datas[k].valueName = this.getValueName(this.datas[k]);
this.datas[k].ts = message.message[j].ts;
isComplete = true;
break;
}
if (isComplete) {
break;
}
};
}
}
}
});
},
//查询物模型列表
getVariableList() {
listThingsModel(this.queryParams).then((res) => {
if (res.code === 200) {
if (this.queryParams.pageNum == 1) {
this.datas = res.rows.map((item) => {
return {
...item,
valueName: this.getValueName(item) || '',
dataTypeName: item.datatype.type || '',
showWay: item.datatype.showWay,
};
});
} else {
this.datas = this.datas.concat(res.rows.map((item) => {
return {
...item,
valueName: this.getValueName(item) || '',
dataTypeName: item.datatype.type || '',
showWay: item.datatype.showWay,
};
}));
}
this.total = res.total;
setTimeout(() => {
if ((this.queryParams.pageNum - 1) * this.queryParams.pageSize >= this.total) {
this.loadmoreStatus = 'nomore';
} else {
this.loadmoreStatus = 'loadmore';
}
}, 1000);
}
});
},
handleConfirmOperator(item) {
this.models = item;
for (let key in this.funVal) {
this.funVal[key] = this.models.value;
}
this.isShowModel = false;
},
//搜索
handleSearch() {
this.datas = [];
this.queryParams.pageNum = 1;
this.getVariableList();
},
handleClearSearch() {
this.handleSearch();
},
// 判断输入是否超过范围
justNumber(item, val) {
if (item.max < val || item.min > val) {
this.isCanSend = false
} else {
this.isCanSend = true
}
this.$forceUpdate()
},
// 采集所有数据
handleCollectionAll() {
const { status, serialNumber } = this.device;
if (status !== 3) {
let title = '';
if (status === 1) {
title = this.$tt('variable.041-8');
} else if (status === 2) {
title = this.$tt('variable.041-9');
} else if (status === 4) {
title = this.$tt('variable.041-10');
}
uni.showToast({
icon: 'none',
title: title
});
return;
}
this.isCollect = true;
const params = {
type: 2,
serialNumber: serialNumber,
};
this.collectParams = { ...params };
},
// 主动采集数据
handleActiveCollect(item) {
const { status, serialNumber } = this.device;
if (status !== 3) {
let title = '';
if (status === 1) {
title = this.$tt('variable.041-8');
} else if (status === 2) {
title = this.$tt('variable.041-9');
} else if (status === 4) {
title = this.$tt('variable.041-10');
}
uni.showToast({
icon: 'none',
title: title
});
return;
}
this.isCollect = true;
const params = {
type: 1,
serialNumber: item.serialNumber,
identifier: item.identifier,
};
this.collectParams = { ...params };
},
//确认采集
handleCollectConfirm() {
propGet(this.collectParams).then(res => {
let title = '';
if (res.code === 200) {
title = this.$tt('variable.041-11');
} else {
title = res.msg;
}
uni.showToast({
icon: 'none',
title: res.msg
});
this.isCollect = false;
})
},
//指令下发
async handleIssueInstruction(item) {
const params = {
deviceId: this.device.deviceId,
modelId: item.modelId
}
//判断是否有权限
const response = await getOrderControl(params);
if (response.code != 200) {
uni.$u.toast(response.msg);
return;
}
//这里兼容子设备的下发,在网关设备下发的时候选择实际子设备的编号
this.serialNumber = item.serialNumber;
let title = '';
if (this.device.status !== 3 && this.device.isShadow !== 1) {
if (this.device.status === 1) {
title = this.$tt('variable.041-8');
} else if (this.device.status === 2) {
title = this.$tt('variable.041-9');
} else {
title = this.$tt('variable.041-10');
}
uni.$u.toast(title);
return;
}
this.isIssue = true;
this.models = {};
this.isCanSend = true;
this.chooseFun = item;
this.getIssueOpations(item);
},
// 封装操作列表
getIssueOpations(item) {
this.issueOpations = [];
let options = [];
this.funVal = {};
const datatype = item.datatype;
if (datatype.type == 'enum') {
options = datatype.enumList?.map(option => {
return {
label: option.text,
value: option.value + ''
}
}) || []
}
if (datatype.type == 'bool') {
options = [{
label: datatype.falseText || '',
value: '0'
}, {
label: datatype.trueText || '',
value: '1'
}]
}
this.issueOpations.push({
dataTypeName: datatype.type,
arrayType: datatype.arrayType,
label: item.modelName,
key: item.identifier,
max: parseInt(datatype?.max || 100),
min: parseInt(datatype?.min || -100),
options: options,
value: item.value
})
this.issueOpations.forEach(item => {
let value = item.value
if (item.datatype == 'integer' || item.datatype == 'decimal' || (item.dataTypeName ==
'array' && item.arrayType == 'integer') || (item.dataTypeName == 'array' && item
.arrayType == 'decimal')) {
value = parseInt(value)
}
this.funVal[item.key] = value;
})
},
// 发送指令
async handleSendService() {
const params = this.funVal;
const key = Object.keys(params)[0];
if (this.isCanSend && !!params[key]) {
try {
const pas = {
serialNumber: this.serialNumber,
identifier: this.chooseFun.identifier,
remoteCommand: params
}
this.isIssue = true;
if (this.deviceInfo.protocolCode === 'MODBUS-RTU' || this.deviceInfo.protocolCode ===
'MODBUS-TCP') {
const res = await serviceInvokeReply(pas);
if (res.code == 200) {
uni.showToast({
icon: 'success',
title: this.$tt('variable.041-11')
});
this.matchParams();
this.isIssue = false;
} else {
uni.showToast({
icon: 'none',
title: res.data
});
this.isIssue = false;
}
} else {
const res = await serviceInvoke(pas);
if (res.code == 200) {
uni.showToast({
icon: 'success',
title: this.$tt('variable.041-11')
});
this.matchParams();
this.isIssue = false;
} else {
uni.showToast({
icon: 'none',
title: res.data
});
this.isIssue = false;
}
}
} finally {}
} else {
uni.showToast({
icon: 'none',
title: this.$tt('variable.041-12')
});
}
},
// 下发后匹配
matchParams() {
for (let i = 0; i < this.datas.length; i++) {
if (this.datas[i].identifier == this.chooseFun.identifier) {
const variable = Object.values(this.funVal)[0];
if (this.device.isShadow == 1 && this.device.status != 3) {
this.datas[i].shadow = variable;
this.datas[i].valueName = this.getValueName(this.datas[i]);
} else {
this.datas[i].value = variable;
this.datas[i].valueName = this.getValueName(this.datas[i]);
}
break;
}
}
},
getValueName(item) {
//返回的数据
let res = item.value || '';
//需要解析的类型
const optionsType = ['bool', 'enum'];
//如果有datatype并且需要解析就遍历解析
if (item.datatype) {
switch (item.datatype.type) {
case 'bool':
if (0 == item.value) res = item.datatype.falseText;
if (1 == item.value) res = item.datatype.trueText;
break;
case 'enum':
item.datatype.enumList?.some((enumOpt) => {
if (enumOpt.value == item.value) {
res = enumOpt.text;
return true;
}
});
break;
}
}
return res;
},
// 属性弹框
handleOpenModel() {
this.isShowModel = true;
},
// 上拉加载
loadMoreLogs() {
this.loadmoreStatus = 'loading';
this.queryParams.pageNum = ++this.queryParams.pageNum;
// 模拟网络请求
setTimeout(() => {
if ((this.queryParams.pageNum - 1) * this.queryParams.pageSize >= this.total) {
this.loadmoreStatus = 'nomore';
} else {
this.loadmoreStatus = 'loading';
this.getVariableList();
}
}, 1000);
},
// 下拉刷新
onPullDownRefresh() {
this.datas = [];
this.queryParams.pageNum = 1;
// 模拟网络请求
setTimeout(x => {
this.type === 1 && this.getList();
uni.stopPullDownRefresh();
}, 1000);
},
}
}
</script>
<style lang="scss" scoped>
.status-variable {
width: 100%;
.nav-bar {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
height: 84rpx;
.right-wrap {
margin-left: 16rpx;
::v-deep .u-icon__icon {
top: -0.5px !important;
margin-right: 0 !important;
}
}
}
.event-wrap {
width: 100%;
margin-top: 22rpx;
.item {
position: relative;
display: flex;
flex-direction: row;
align-items: center;
border-radius: 10px;
padding: 50rpx 30rpx 30rpx;
background-color: #ffffff;
&:not(:last-child) {
margin-bottom: 20rpx;
}
.status {
position: absolute;
top: 0;
left: 0;
font-size: 24rpx;
color: #ffffff;
padding: 4rpx 14rpx;
border-radius: 20rpx 0 20rpx 0;
}
.left {
flex: 1;
.name {
font-size: 30rpx;
line-height: 44rpx;
}
.time {
font-size: 24rpx;
line-height: 40rpx;
margin-top: 16rpx;
}
}
.right {
display: flex;
flex-direction: row;
.data {
font-size: 28rpx;
line-height: 40rpx;
width: 220rpx;
}
.icon {
display: flex;
flex-direction: row;
gap: 30rpx;
}
}
}
}
}
.other {
.issue-popup {
padding: 20rpx;
margin-bottom: 100rpx;
.nav {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
font-size: 32rpx;
margin-bottom: 34rpx;
}
.with-range {
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
}
::v-deep .u-form {
background: #ffffff;
padding: 0 20rpx;
border-radius: 16rpx;
}
.model-popup {
margin-bottom: 100rpx;
padding: 10rpx 0;
.cell-group-wrap {
background: #eef3f7;
.cell-wrap {
text-align: center;
background: #fff;
padding: 5rpx 0;
border-bottom: 1rpx solid #f8f8f8;
&:last-child {
margin-top: 15rpx;
}
}
}
}
}
}
</style>