| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505 |
- <template>
- <view class="control-container">
- <custom-card>
- <block slot="backText">{{ title }}</block>
- </custom-card>
- <view class="control-content" v-if="leftList.length > 0">
- <view class="control-list-left">
- <view
- class="control-list-left-item"
- v-for="(item, index) in leftList"
- :key="index"
- @click="controlClick(index)"
- :class="{ 'control-list-left-item-active': index === activeIndex }"
- >
- <image
- :src="borderRadius"
- class="borderTopRadius"
- v-if="index === activeIndex"
- />
- {{ item.sfDisplayname }}
- <image
- :src="borderRadius"
- class="borderBottomRadius"
- v-if="index === activeIndex"
- />
- </view>
- </view>
- <view class="control-list-right">
- <view
- v-for="(item, index) in deviceList"
- class="control-list-right-item"
- :class="item.sfType == '10' ? 'control-list-right-items' : ''"
- :key="index"
- >
- <view style="display: flex; align-items: center;" v-if="item.sfType != '10'">
- <view class="control-list-right-item-icon">
- <image
- :src="item.sfType == '3' ? stir : solenoidValve"
- class="solenoid-valve"
- />
- </view>
- <view class="control-list-right-item-title">{{
- item.sfDisplayname || item.sfName
- }}</view>
- <view class="control-list-right-item-action">
- <u-switch
- :value="item.value == 1 ? true : false"
- @change="changeSwitch(item)"
- activeColor="#14A478"
- size="40"
- inactiveColor="rgb(230, 230, 230)"
- ></u-switch>
- </view>
- </view>
- <view v-else style="width:100%">
- <view class="control-list-right-item-icon">
- <image
- :src="getSrc(item)"
- class="solenoid-valve"
- />
- <text style="margin-left:6rpx">
- {{
- item.sfDisplayname || item.sfName
- }}
- </text>
- </view>
- <view class="control-list-right-item-actions">
- <text style="color:#999999">状态 </text>
- <view class="control-list-right-item-actions-status">
- <view
- class="status-item"
- :class="item.value == '1'?'status-item-open':''"
- @click="changeStatus(item, '1')"
- >开</view>
- <view
- class="status-item"
- :class="item.value == '0'?'status-item-pause':''"
- @click="changeStatus(item, '0')"
- >停</view>
- <view
- class="status-item status-item-close-label"
- :class="item.value == '2'?'status-item-close':''"
- @click="changeStatus(item, '2')"
- >关</view>
- </view>
- </view>
- </view>
- </view>
- <u-empty v-if="deviceList.length === 0" text="暂无数据"></u-empty>
- </view>
- </view>
- <u-empty v-else text="暂无数据"></u-empty>
- </view>
- </template>
- <script>
- import solenoidValve from './assets/solenoidValve.png';
- import stir from './assets/stir.png';
- import solenoidValveGray from './assets/solenoidValveGray.png';
- import solenoidValveStop from './assets/solenoidValveStop.png';
- import borderRadius from './assets/borderRadius.png';
- export default {
- name: 'control',
- data() {
- return {
- activeIndex: 0,
- value: false,
- solenoidValve,
- solenoidValveGray,
- solenoidValveStop,
- stir,
- borderRadius,
- title: '控制面板',
- leftList: [],
- deviceList: [],
- devBid: '',
- ws: null,
- heartbeatTimer: null,
- webSockedData: null,
- dataArray: [],
- info: null,
- sfToken: '',
- entityId: '',
- reconnectCount: 0,
- };
- },
- methods: {
- getSrc(item){
- if(item.value == '1'){
- return this.solenoidValve;
- }else if(item.value == '0'){
- return this.solenoidValveStop;
- }else if(item.value == '2'){
- return this.solenoidValveGray;
- }
- },
- initWebSocket() {
- const url = 'wss://things.ysiot.net:18080/api/ws';
- this.ws = uni.connectSocket({
- url: url,
- success: () => {
- console.log('WebSocket 连接初始化成功');
- }
- });
- uni.onSocketOpen(() => {
- console.log('WebSocket 已连接');
- // 发送测试参数
- const testParams = {
- cmds: [
- {
- type: 'TIMESERIES',
- entityType: 'DEVICE',
- entityId: this.entityId,
- scope: 'LATEST_TELEMETRY',
- cmdId: 1
- }
- ],
- authCmd: {
- cmdId: 0,
- token: this.sfToken
- }
- };
- console.log('发送测试参数:', testParams);
- uni.sendSocketMessage({
- data: JSON.stringify(testParams)
- });
- // 心跳:每 30 秒 ping 一次
- this.heartbeatTimer = setInterval(() => {
- uni.sendSocketMessage({
- data: JSON.stringify({ type: 'ping' })
- });
- }, 30 * 1000);
- });
- uni.onSocketMessage((evt) => {
- try {
- const data = JSON.parse(evt.data);
- this.webSockedData = data;
- // 收到实时数据后刷新右侧组件
- if(this.dataArray.length){
- this.mergeTwoObject(this.dataArray,this.webSockedData?.data);
- this.initData(this.dataArray);
- }
- } catch (e) {
- console.error('WebSocket 消息解析失败', e);
- }
- });
- uni.onSocketError((e) => {
- console.error('WebSocket 错误', e);
- });
- uni.onSocketClose(() => {
- console.warn('WebSocket 已断开,3 秒后尝试重连');
- clearInterval(this.heartbeatTimer);
- this.reconnectCount = (this.reconnectCount || 0) + 1;
- if (this.reconnectCount <= 10) {
- setTimeout(() => this.initWebSocket(), 3000);
- } else {
- console.warn('WebSocket 重连次数已达上限,停止重连');
- }
- });
- },
- getChecked(item) {
- return item.value == 1 ? true : false;
- },
-
- mergeTwoObject(firstObject, secondObject) {
- // 构建 sfCode 到对象的映射,实现 O(1) 查找
- const sfCodeMap = new Map();
-
- // 递归构建映射
- const buildMap = (items) => {
- for (const item of items) {
- if (item.sfCode) {
- sfCodeMap.set(item.sfCode, item);
- }
- if (item.childrenList && item.childrenList.length) {
- buildMap(item.childrenList);
- }
- }
- };
-
- buildMap(firstObject);
-
- // 遍历 secondObject 并更新值
- for (const key in secondObject) {
- let newKey = '';
- if(key.includes('GHChannelDev:ChannelParamSet')){
- newKey = key + ':Status';
- const item = sfCodeMap.get(newKey);
- if (item) {
- const params = JSON.parse(secondObject[key][0][1]);
- this.$set(item,'value',params.Status)
- }
- }else{
- const item = sfCodeMap.get(key);
- if (item) {
- this.$set(item,'value',secondObject[key][0][1])
- }
- }
- }
- },
- async sfDecvtl(data){
- const res = await this.$myRequest({
- url: '/api/v2/iot/mobile/device/sf/devctl/',
- method: 'post',
- data,
- header: {
- 'Content-Type': 'application/json',
- },
- });
- if (res.code !== '000000') {
- item.value = item.value == 1 ? 0 : 1;
- }
- uni.showToast({
- title: res.msg,
- icon: 'none',
- });
- },
- changeStatus(item,index){
- const sfCode = item.sfCode;
- const params = {};
- params[sfCode] = index;
- const data = {
- devBid: this.devBid,
- data: params,
- };
- this.sfDecvtl(data)
- },
- changeSwitch(item) {
- item.value = item.value == 1 ? 0 : 1;
- const sfCode = item.sfCode;
- const params = {};
- params[sfCode] = item.value;
- const data = {
- devBid: this.devBid,
- data: params,
- };
- this.sfDecvtl(data)
- },
- controlClick(index) {
- this.activeIndex = index;
- this.deviceList = this.leftList[this.activeIndex]?.childrenList || [];
- },
- /*
- 0 水源 泵类 负责水源进入管道的总泵或者阀
- 1 肥料 泵类 负责肥料进入管道的总阀或者泵
- 2 吸肥 泵类 控制肥料桶出肥的阀或者泵,每个肥料桶一个
- 3 搅拌 泵类 肥料桶的搅拌电机或者泵 每个肥料桶一个
- 4 肥料桶 泵类 肥料桶,每个施肥机有多个或者没有,不一定真实存在,只是逻辑上的概念
- 5 电磁阀 无 管道最末端每个田地里控制出水的阀门
- 6 传感器 无 水肥机上的 温度,压力,流速,PH EC等监测类要素
- 7 灌区 无 逻辑区域,电磁阀的分组
- */
- async getsfrhinfo() {
- const res = await this.$myRequest({
- url: '/api/v2/iot/device/sf/info/',
- method: 'post',
- data: {
- devBid: String(this.devBid),
- },
- });
- this.info = res;
- this.sfToken = this.info?.sfToken;
- this.entityId = this.info?.sfUuid;
- this.closeWebSocket();
- this.initWebSocket();
- },
- // 关闭 WebSocket
- closeWebSocket() {
- clearInterval(this.heartbeatTimer);
- if (this.ws) {
- uni.closeSocket();
- this.ws = null;
- }
- },
- initData(res){
- this.deviceList = [];
- const sfCurrent = [
- {
- sfDisplayname: '水肥机',
- childrenList: [],
- },
- ];
- const irrigatedAreaList = [];
- res?.forEach((item) => {
- if (item.sfType === '0' || item.sfType === '1' || item.sfType === '2') {
- sfCurrent[0].childrenList.push(item);
- } else if (item.sfType === '4') {
- const childrenList = item?.childrenList || [];
- childrenList.forEach((child) => {
- if (
- child.sfType === '3' ||
- child.sfType === '2' ||
- child.sfType === '8'
- ) {
- sfCurrent[0].childrenList.push(child);
- }
- });
- } else if (item.sfType === '7') {
- irrigatedAreaList.push(item);
- }
- });
- this.leftList = [...sfCurrent, ...irrigatedAreaList];
- this.deviceList = this.leftList[this.activeIndex]?.childrenList || [];
- },
- async getdeviceSfStatus() {
- const res = await this.$myRequest({
- url: '/api/v2/iot/mobile/device/sf/status/',
- method: 'post',
- data: {
- devBid: this.devBid,
- },
- });
- this.dataArray = res;
- this.initData(res);
- },
- },
- onLoad(options) {
- const { devBid } = options;
- this.devBid = devBid;
- this.getdeviceSfStatus();
- this.getsfrhinfo();
- },
- };
- </script>
- <style scoped lang="scss">
- uni-page-body {
- position: relative;
- height: 100%;
- }
- .control-container {
- background: linear-gradient(180deg, #ffffff00 0%, #fff 23.64%, #fff 100%),
- linear-gradient(102deg, #bfeadd 6.77%, #b8f1e7 40.15%, #b9eef5 84.02%);
- height: 100%;
- width: 100%;
- overflow-x: hidden;
- overflow-y: hidden;
- .control-content {
- display: flex;
- width: 100%;
- height: calc(100% - 102rpx);
- overflow-x: hidden;
- overflow-y: auto;
- padding: 8px 0;
- border-radius: 8px 8px 0 0;
- background: #fff;
- .control-list-left {
- width: 250rpx;
- position: fixed;
- height: calc(100% - 90rpx);
- overflow-x: hidden;
- overflow-y: auto;
- .control-list-left-item {
- width: 240rpx;
- height: 96rpx;
- line-height: 96rpx;
- background: #f5f7fa;
- padding-left: 24rpx;
- color: #364d46;
- font-family: 'Source Han Sans CN VF';
- font-size: 32rpx;
- font-weight: 400;
- position: relative;
- }
- .control-list-left-item-active {
- background: #ffffff;
- border-radius: 0 -16rpx -16rpx 0;
- }
- .borderTopRadius {
- position: absolute;
- width: 40rpx;
- height: 40rpx;
- right: 10rpx;
- top: -32rpx;
- transform: rotate(90deg);
- z-index: 100;
- }
- .borderBottomRadius {
- position: absolute;
- z-index: 100;
- width: 40rpx;
- height: 40rpx;
- bottom: -32rpx;
- right: 10rpx;
- }
- }
- .control-list-right {
- width: calc(100% - 250rpx);
- margin: 0 32rpx;
- margin-left: 280rpx;
- .control-list-right-item {
- height: 96rpx;
- display: flex;
- padding: 0rpx 16rpx;
- align-items: center;
- border-radius: 8rpx;
- background: #f5f7fa;
- margin-bottom: 22rpx;
- position: relative;
- .control-list-right-item-icon {
- display: flex;
- align-items: center;
- .solenoid-valve {
- width: 40rpx;
- height: 40rpx;
- }
- }
- .control-list-right-item-title {
- margin-left: 8rpx;
- }
- .control-list-right-item-action {
- position: absolute;
- right: 16rpx;
- }
- .control-list-right-item-actions {
- display: flex;
- align-items: center;
- margin-top: 22rpx;
- justify-content: space-between;
- .control-list-right-item-actions-status {
- display: flex;
- align-items: center;
- border-radius: 12rpx;
- background: #FFF;
- padding: 6rpx;
- margin-left: 12rpx;
- height: 50rpx;
- .status-item{
- width: 92rpx;
- height: 50rpx;
- line-height: 50rpx;
- text-align: center;
- border-radius: 8rpx;
- background: #FFF;
- color:#656565;
- }
- .status-item-close-label{
- color:#FF5951;
- }
- .status-item-open{
- background: #0BBC58;
- color: #fff;
- }
- .status-item-pause{
- background: #F8B610;
- color: #fff;
- }
- .status-item-close{
- background: #DDDFE6;
- color: #303133;
- }
- }
- }
- }
- .control-list-right-items{
- height: 172rpx;
- }
- }
- }
- }
- </style>
|