| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417 |
- <template>
- <view class="greenhouse-page">
- <custom-card>
- <block slot="backText">
- <view class="greenhouse-top__title">
- {{ dataSource.devName }}
- </view> </block
- ><view slot="right">
- <!-- <view class="ntoNative" @tap="toNavigation">
- <image :src="location" class="icon"></image>
- </view> -->
- </view>
- </custom-card>
- <view class="greenhouse-top">
- <device-card :dataSource="dataSource" @navigation="toNavigation" />
- </view>
- <view class="tabs">
- <view
- class="tab-item"
- :class="{ active: currentTab === 'env' }"
- @click="currentTab = 'env'"
- >环境要素</view
- >
- <view
- class="tab-item"
- :class="{ active: currentTab === 'channel' }"
- @click="currentTab = 'channel'"
- >大棚控制</view
- >
- <view
- class="tab-item"
- :class="{ active: currentTab === 'record' }"
- @click="currentTab = 'record'"
- >操作记录</view
- >
- </view>
- <!-- 大棚控制:通道卡片 -->
- <view class="greenhouse-bottom" v-if="currentTab === 'channel'">
- <view class="greenhouse-bottom-header">
- 大棚控制
- </view>
- <operation-card
- @change="channelControlHandler"
- v-for="(item, index) in juanmoList"
- :key="index"
- :dourceData="item"
- />
- </view>
- <!-- 环境要素:传感器数据 -->
- <view class="greenhouse-bottom" v-else-if="currentTab === 'env'">
- <Element :sensorList="sensorList" title="环境要素" />
- </view>
- <!-- 操作记录 -->
- <view class="greenhouse-bottom" v-else>
- <RecordList :devBid="devBid" />
- </view>
- </view>
- </template>
- <script>
- import Element from './components/element.vue';
- import deviceCard from './components/DeviceCard.vue';
- import OperationCard from './components/OperationCard.vue';
- import RecordList from './components/RecordList.vue';
- export default {
- components: {
- Element,
- deviceCard,
- OperationCard,
- RecordList,
- },
- data() {
- return {
- location:
- 'https://s3.hnyfwlw.com/webstaticimg/bigdata_app/image/zhongshui/location.svg',
- ws: null,
- sfToken: '',
- entityId: '',
- heartbeatTimer: null,
- dataArray: [],
- webSockedData: {},
- reconnectCount: 0,
- manualClosed: false,
- currentTab: 'channel',
- juanmoList: [],
- sensorList: [],
- dataSource: {
- devStatus: '1',
- devStatusName: '在线',
- devName: '-',
- devCode: '-',
- devUpdateddate: '-',
- devProvincealign: '-',
- },
- };
- },
- methods: {
- init() {
- this.getKzgYunshangStatus();
- this.getYunshangInfo();
- },
- // 通道控制:三状态 value 0/1/2,两状态 value 0/1
- channelControlHandler(prop) {
- const { dourceData, code, value, field } = prop;
- // 本地先行更新开关状态,稍后拉取接口校正
- this.$set(dourceData, field || 'value', value);
- this.optDevctl({
- devBid: this.devBid,
- data: {
- [code]: value,
- },
- });
- },
- async optDevctl(payload) {
- const res = await this.$myRequest({
- url: '/api/v2/iot/device/kzg/yunshang/devctl/',
- method: 'post',
- data: payload,
- header: {
- 'Content-Type': 'application/json',
- },
- });
- // 提示
- uni.showToast({
- title: res?.msg || '',
- icon: 'none',
- duration: 3000,
- });
- setTimeout(() => {
- this.getKzgYunshangStatus();
- }, 1000);
- },
- // 获取设备详情(sfToken 用于 WebSocket 鉴权)
- async getYunshangInfo() {
- const res = await this.$myRequest({
- url: '/api/v2/iot/device/kzg/yunshang/info/',
- method: 'post',
- data: {
- devBid: this.devBid,
- },
- });
- this.dataSource = res;
- this.sfToken = res?.sfToken;
- this.entityId = res?.sfUuid;
- this.closeWebSocket();
- this.initWebSocket();
- },
- toNavigation() {
- const roundedLongitude =
- this.dataSource.devLngalign || this.dataSource.devLng;
- const roundedLatitude =
- this.dataSource.devLatalign || this.dataSource.devLat;
- uni.openLocation({
- latitude: Number(roundedLatitude || 0),
- longitude: Number(roundedLongitude || 0),
- name: '',
- address: '',
- scale: 18, // 地图缩放级别
- success: () => console.log('导航启动成功'),
- fail: (err) => console.error('导航失败:', err),
- });
- // if (!roundedLongitude) {
- // uni.showToast({
- // title: '当前设备无定位信息',
- // icon: 'none',
- // duration: 3000,
- // });
- // return;
- // }
- // if (
- // plus.runtime.isApplicationExist({
- // pname: 'com.autonavi.minimap',
- // action: 'iosamap://',
- // })
- // ) {
- // let url = `amapuri://route/plan?sourceApplication=yourAppName&dlat=${roundedLatitude}&dlon=${roundedLongitude}&dev=0&t=0&style=0&start=&auto=1&rtdType=1&coordinate=wgs84`;
- // plus.runtime.openURL(url);
- // } else if (
- // plus.runtime.isApplicationExist({
- // pname: 'com.baidu.BaiduMap',
- // action: 'baidumap://',
- // })
- // ) {
- // let url = `baidumap://map/direction?destination=${roundedLongitude},${roundedLatitude}&mode=navigation&output=html&coord_type=wgs84`;
- // plus.runtime.openURL(url);
- // } else {
- // uni.showToast({
- // title: '当前未安装 高德地图或百度地图 无法导航',
- // icon: 'none',
- // duration: 3000,
- // });
- // }
- },
- // 获取控制柜状态信息(只取 channel_list,sensor_list 不处理)
- async getKzgYunshangStatus() {
- const res = await this.$myRequest({
- url: '/api/v2/iot/device/kzg/yunshang/status/',
- method: 'post',
- data: {
- devBid: this.devBid,
- },
- });
- this.dataArray = res?.channel_list || [];
- this.juanmoList = this.dataArray;
- this.sensorList = res?.sensor_list || [];
- },
- initWebSocket() {
- this.manualClosed = false;
- const url = 'wss://things.ysiot.net:18080/api/ws';
- this.ws = uni.connectSocket({
- url: url,
- success: () => {
- console.log('WebSocket 连接初始化成功');
- },
- });
- uni.onSocketOpen(() => {
- console.log('WebSocket 已连接');
- // 订阅设备实时遥测,鉴权 token 来自 kzg/yunshang/info 接口
- const params = {
- cmds: [
- {
- type: 'TIMESERIES',
- entityType: 'DEVICE',
- entityId: this.entityId,
- scope: 'LATEST_TELEMETRY',
- cmdId: 1,
- },
- ],
- authCmd: {
- cmdId: 0,
- token: this.sfToken,
- },
- };
- uni.sendSocketMessage({
- data: JSON.stringify(params),
- });
- // 心跳:每 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);
- }
- } catch (e) {
- console.error('WebSocket 消息解析失败', e);
- }
- });
- uni.onSocketError((e) => {
- console.error('WebSocket 错误', e);
- });
- uni.onSocketClose(() => {
- console.warn('WebSocket 已断开,3 秒后尝试重连');
- clearInterval(this.heartbeatTimer);
- if (this.manualClosed) {
- return;
- }
- this.reconnectCount = (this.reconnectCount || 0) + 1;
- if (this.reconnectCount <= 10) {
- setTimeout(() => this.initWebSocket(), 3000);
- } else {
- console.warn('WebSocket 重连次数已达上限,停止重连');
- }
- });
- },
- // 用 WebSocket 实时数据按 ChannelParamSetXX:Status 匹配 channel_list 更新状态
- mergeTwoObject(firstObject, secondObject) {
- // 构建 key 到通道的映射,实现 O(1) 查找
- const codeMap = new Map();
- const buildMap = (items) => {
- for (const item of items) {
- // Status key:优先同名属性,其次 code/sfCode 标识(值在 value 里)
- let statusKey = Object.keys(item).find((k) =>
- /^ChannelParamSet\d+:Status$/.test(k)
- );
- let statusField = statusKey || '';
- if (!statusKey) {
- statusKey = [item.code, item.sfCode].find((v) =>
- /^ChannelParamSet\d+:Status$/.test(String(v || ''))
- );
- statusField = 'value';
- }
- if (statusKey) {
- codeMap.set(statusKey, { item, statusField });
- }
- }
- };
- buildMap(firstObject);
- // 遍历 secondObject 并更新值
- for (const key in secondObject) {
- let target = codeMap.get(key);
- // 兼容不带后缀的上报 key
- if (!target && /^ChannelParamSet\d+$/.test(key)) {
- target = codeMap.get(`${key}:Status`);
- }
- if (!target) {
- continue;
- }
- const val = secondObject[key]?.[0]?.[1];
- if (val === undefined) {
- continue;
- }
- this.$set(target.item, target.statusField, val);
- }
- },
- // 关闭 WebSocket
- closeWebSocket() {
- this.manualClosed = true;
- clearInterval(this.heartbeatTimer);
- if (this.ws) {
- uni.closeSocket();
- this.ws = null;
- }
- },
- },
- onLoad(query) {
- this.devBid = query.devBid;
- this.devBid && this.init();
- },
- onUnload() {
- this.manualClosed = true;
- this.closeWebSocket();
- },
- };
- </script>
- <style scoped lang="scss">
- uni-page-body {
- position: relative;
- height: 100%;
- }
- .icon {
- width: 40rpx;
- height: 40rpx;
- }
- .tabs {
- display: flex;
- align-items: center;
- margin: 24rpx 30rpx 0;
- padding: 8rpx 16rpx;
- background: #ffffff;
- border-radius: 24rpx;
- .tab-item {
- flex: 1;
- height: 80rpx;
- display: flex;
- align-items: center;
- justify-content: center;
- position: relative;
- color: #999999;
- font-family: 'Source Han Sans CN VF';
- font-size: 28rpx;
- font-weight: 400;
- &.active {
- color: #042118;
- font-weight: 700;
- &::after {
- content: '';
- position: absolute;
- bottom: 8rpx;
- left: 50%;
- transform: translateX(-50%);
- width: 48rpx;
- height: 8rpx;
- border-radius: 4rpx;
- background: #0bbc58;
- }
- }
- }
- }
- .greenhouse-page {
- background: linear-gradient(
- 180deg,
- #ffffff00 0%,
- #eff2fa 23.64%,
- #eff2fa 100%
- ),
- linear-gradient(102deg, #bfeadd 6.77%, #b8f1e7 40.15%, #b9eef5 84.02%);
- min-height: 100vh;
- width: 100%;
- overflow-x: hidden;
- overflow-y: scroll;
- .greenhouse-top {
- text-align: center;
- margin: 0 auto;
- margin-top: 18rpx;
- padding: 0 30rpx;
- }
- .greenhouse-bottom {
- width: calc(100% - 128rpx);
- padding: 32rpx 32rpx 1rpx 32rpx;
- background: #ffffff;
- border-radius: 16rpx;
- margin: 0 auto;
- margin-top: 32rpx;
- .greenhouse-bottom-header {
- color: #042118;
- font-family: 'Source Han Sans CN VF';
- font-size: 28rpx;
- font-weight: 700;
- margin-bottom: 24rpx;
- }
- }
- }
- </style>
|