Przeglądaj źródła

feat(水肥一体机): 新增自动控制、定时设置和配方设置功能

添加自动控制页面实现灌溉模式、配方方式和轮灌组配置
实现定时设置功能,支持多个定时器配置
新增配方设置页面,支持肥源选择和肥料配置
在控制页和水肥机主页添加WebSocket实时数据更新
更新路由配置添加新页面路径
allen 2 tygodni temu
rodzic
commit
259334de78

+ 24 - 0
pages.json

@@ -137,6 +137,30 @@
 					}
 				},
 				{
+					"path": "autoSetting",
+					"style": {
+						"navigationBarTitleText": "",
+						"enablePullDownRefresh": false,
+						"navigationStyle": "custom"
+					}
+				},
+				{
+					"path": "formulaSetting",
+					"style": {
+						"navigationBarTitleText": "",
+						"enablePullDownRefresh": false,
+						"navigationStyle": "custom"
+					}
+				},
+				{
+					"path": "timingSetting",
+					"style": {
+						"navigationBarTitleText": "",
+						"enablePullDownRefresh": false,
+						"navigationStyle": "custom"
+					}
+				},
+				{
 					"path": "history",
 					"style": {
 						"navigationBarTitleText": "",

+ 484 - 0
pages/cb/shuifeizsFirst/autoSetting.vue

@@ -0,0 +1,484 @@
+<template>
+  <view class="auto-fertilization-container">
+    <!-- 顶部导航栏 -->
+    <view class="nav-bar">
+      <view class="nav-left">
+        <text class="back-icon">←</text>
+      </view>
+      <view class="nav-center">
+        <text class="nav-title">自动施肥</text>
+      </view>
+      <view class="nav-right">
+        <text class="nav-icon">⋯</text>
+        <text class="nav-icon">⚙️</text>
+      </view>
+    </view>
+
+    <!-- 内容区域 -->
+    <view class="content">
+      <!-- 灌溉模式 -->
+      <view class="setting-item">
+        <text class="setting-label">灌溉模式</text>
+        <view class="radio-group">
+          <label class="radio-item" :class="{ active: irrigationMode === '定时' }" @click="irrigationMode = '定时'">
+            <radio value="定时" :checked="irrigationMode === '定时'" color="#14a478" />
+            <text>定时</text>
+          </label>
+          <label class="radio-item" :class="{ active: irrigationMode === '定量' }" @click="irrigationMode = '定量'">
+            <radio value="定量" :checked="irrigationMode === '定量'" color="#14a478" />
+            <text>定量</text>
+          </label>
+          <label class="radio-item" :class="{ active: irrigationMode === '禁用' }" @click="irrigationMode = '禁用'">
+            <radio value="禁用" :checked="irrigationMode === '禁用'" color="#14a478" />
+            <text>禁用</text>
+          </label>
+        </view>
+      </view>
+
+      <!-- 配方方式 -->
+      <view class="setting-item">
+        <text class="setting-label">配方方式</text>
+        <view class="radio-group">
+          <label class="radio-item" :class="{ active: formulaMode === 'PID' }" @click="formulaMode = 'PID'">
+            <radio value="PID" :checked="formulaMode === 'PID'" color="#14a478" />
+            <text>PID</text>
+          </label>
+          <label class="radio-item" :class="{ active: formulaMode === '水肥比例' }" @click="formulaMode = '水肥比例'">
+            <radio value="水肥比例" :checked="formulaMode === '水肥比例'" color="#14a478" />
+            <text>水肥比例</text>
+          </label>
+          <label class="radio-item" :class="{ active: formulaMode === '禁用' }" @click="formulaMode = '禁用'">
+            <radio value="禁用" :checked="formulaMode === '禁用'" color="#14a478" />
+            <text>禁用</text>
+          </label>
+        </view>
+      </view>
+
+      <!-- 轮灌次数 -->
+      <view class="setting-item">
+        <text class="setting-label">轮灌次数</text>
+        <view class="number-input">
+          <text class="number-btn" @click="decreaseRoundCount">−</text>
+          <text class="number-value">{{ roundCount }}</text>
+          <text class="number-btn" @click="increaseRoundCount">+</text>
+        </view>
+      </view>
+
+      <!-- 轮灌间隔 -->
+      <view class="setting-item">
+        <text class="setting-label">轮灌间隔</text>
+        <view class="time-input">
+          <input type="number" v-model="roundInterval" class="time-input-field" />
+          <text class="time-unit">分钟</text>
+        </view>
+      </view>
+
+      <!-- 肥前水 -->
+      <view class="setting-item">
+        <text class="setting-label">肥前水</text>
+        <view class="time-input">
+          <input type="number" v-model="waterBefore" class="time-input-field" />
+          <text class="time-unit">分钟</text>
+        </view>
+      </view>
+
+      <!-- 肥后水 -->
+      <view class="setting-item">
+        <text class="setting-label">肥后水</text>
+        <view class="time-input">
+          <input type="number" v-model="waterAfter" class="time-input-field" />
+          <text class="time-unit">分钟</text>
+        </view>
+      </view>
+
+      <!-- 定时轮灌组 -->
+      <view class="round-groups-section">
+        <text class="section-title">定时轮灌组</text>
+        <view class="round-groups-container">
+          <!-- 左侧轮灌组列表 -->
+          <view class="round-groups-list">
+            <view 
+              v-for="i in 7" 
+              :key="i"
+              class="round-group-item"
+              :class="{ active: selectedGroup === i }"
+              @click="selectedGroup = i"
+            >
+              <text>{{ i }}组</text>
+            </view>
+          </view>
+
+          <!-- 右侧轮灌组详情 -->
+          <view class="round-group-detail">
+            <!-- 1组 -->
+            <view class="group-detail-item" :class="{ active: selectedGroup === 1 }">
+              <view class="group-header">
+                <text class="group-title">1组</text>
+                <text class="group-check" v-if="selectedGroup === 1">✓</text>
+              </view>
+              <view class="group-settings">
+                <view class="setting-row">
+                  <text class="setting-row-label">施肥配方</text>
+                  <input type="text" placeholder="请输入" class="setting-row-input" />
+                </view>
+                <view class="setting-row">
+                  <text class="setting-row-label">灌溉时长</text>
+                  <input type="number" v-model="irrigationTime1" class="setting-row-input" />
+                  <text class="setting-row-unit">分钟</text>
+                </view>
+                <view class="setting-row">
+                  <text class="setting-row-label">施肥时长</text>
+                  <input type="number" v-model="fertilizationTime1" class="setting-row-input" />
+                  <text class="setting-row-unit">分钟</text>
+                </view>
+              </view>
+            </view>
+
+            <!-- 2组 -->
+            <view class="group-detail-item" :class="{ active: selectedGroup === 2 }">
+              <view class="group-header">
+                <text class="group-title">2组</text>
+                <text class="group-check" v-if="selectedGroup === 2">✓</text>
+              </view>
+              <view class="group-settings">
+                <view class="setting-row">
+                  <text class="setting-row-label">施肥配方</text>
+                  <input type="text" placeholder="请输入" class="setting-row-input" />
+                </view>
+                <view class="setting-row">
+                  <text class="setting-row-label">灌溉时长</text>
+                  <input type="number" v-model="irrigationTime2" class="setting-row-input" />
+                  <text class="setting-row-unit">分钟</text>
+                </view>
+                <view class="setting-row">
+                  <text class="setting-row-label">施肥时长</text>
+                  <input type="number" v-model="fertilizationTime2" class="setting-row-input" />
+                  <text class="setting-row-unit">分钟</text>
+                </view>
+              </view>
+            </view>
+          </view>
+        </view>
+      </view>
+    </view>
+
+    <!-- 底部确定按钮 -->
+    <view class="confirm-btn-container">
+      <view class="confirm-btn" @click="confirm">
+        <text class="confirm-btn-text">确定</text>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      // 灌溉模式
+      irrigationMode: '定时',
+      // 配方方式
+      formulaMode: 'PID',
+      // 轮灌次数
+      roundCount: 1,
+      // 轮灌间隔(分钟)
+      roundInterval: 0,
+      // 肥前水(分钟)
+      waterBefore: 0,
+      // 肥后水(分钟)
+      waterAfter: 0,
+      // 选中的轮灌组
+      selectedGroup: 1,
+      // 1组设置
+      irrigationTime1: 0,
+      fertilizationTime1: 0,
+      // 2组设置
+      irrigationTime2: 0,
+      fertilizationTime2: 0
+    };
+  },
+  methods: {
+    // 减少轮灌次数
+    decreaseRoundCount() {
+      if (this.roundCount > 1) {
+        this.roundCount--;
+      }
+    },
+    // 增加轮灌次数
+    increaseRoundCount() {
+      this.roundCount++;
+    },
+    // 确认按钮点击事件
+    confirm() {
+      // 这里可以添加确认逻辑
+      console.log('确认设置');
+    }
+  }
+};
+</script>
+
+<style scoped lang="scss">
+.auto-fertilization-container {
+  width: 100%;
+  min-height: 100vh;
+  background: linear-gradient(180deg, #e6f7f2 0%, #f5f5f5 100%);
+  font-family: 'Source Han Sans CN';
+}
+
+/* 顶部导航栏 */
+.nav-bar {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 20rpx 32rpx;
+  background: linear-gradient(180deg, #14a478 0%, #0f8a64 100%);
+  color: #fff;
+
+  .nav-left {
+    .back-icon {
+      font-size: 32rpx;
+    }
+  }
+
+  .nav-center {
+    .nav-title {
+      font-size: 32rpx;
+      font-weight: 500;
+    }
+  }
+
+  .nav-right {
+    display: flex;
+    gap: 24rpx;
+
+    .nav-icon {
+      font-size: 28rpx;
+    }
+  }
+}
+
+/* 内容区域 */
+.content {
+  padding: 32rpx;
+}
+
+/* 设置项 */
+.setting-item {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 24rpx 0;
+  border-bottom: 1rpx solid #e8e8e8;
+
+  .setting-label {
+    font-size: 28rpx;
+    color: #042118;
+  }
+
+  /* 单选组 */
+  .radio-group {
+    display: flex;
+    gap: 32rpx;
+
+    .radio-item {
+      display: flex;
+      align-items: center;
+      gap: 8rpx;
+      font-size: 26rpx;
+      color: #666;
+
+      &.active {
+        color: #14a478;
+      }
+
+      radio {
+        transform: scale(0.8);
+      }
+    }
+  }
+
+  /* 数字输入 */
+  .number-input {
+    display: flex;
+    align-items: center;
+    gap: 16rpx;
+
+    .number-btn {
+      width: 40rpx;
+      height: 40rpx;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      background: #f5f5f5;
+      border-radius: 4rpx;
+      font-size: 28rpx;
+      color: #666;
+    }
+
+    .number-value {
+      min-width: 60rpx;
+      text-align: center;
+      font-size: 26rpx;
+      color: #042118;
+    }
+  }
+
+  /* 时间输入 */
+  .time-input {
+    display: flex;
+    align-items: center;
+    gap: 8rpx;
+
+    .time-input-field {
+      width: 100rpx;
+      height: 52rpx;
+      padding: 0 16rpx;
+      font-size: 26rpx;
+      text-align: center;
+      border: 1rpx solid #d9d9d9;
+      border-radius: 8rpx;
+      color: #042118;
+    }
+
+    .time-unit {
+      font-size: 24rpx;
+      color: #666;
+    }
+  }
+}
+
+/* 轮灌组区域 */
+.round-groups-section {
+  margin-top: 40rpx;
+
+  .section-title {
+    font-size: 28rpx;
+    font-weight: 500;
+    color: #042118;
+    margin-bottom: 24rpx;
+  }
+
+  .round-groups-container {
+    display: flex;
+    background: #fff;
+    border-radius: 12rpx;
+    box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
+
+    /* 左侧轮灌组列表 */
+    .round-groups-list {
+      width: 120rpx;
+      border-right: 1rpx solid #e8e8e8;
+
+      .round-group-item {
+        height: 80rpx;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        font-size: 26rpx;
+        color: #666;
+        border-bottom: 1rpx solid #f0f0f0;
+
+        &.active {
+          background: #e6f7f2;
+          color: #14a478;
+          font-weight: 500;
+        }
+
+        &:last-child {
+          border-bottom: none;
+        }
+      }
+    }
+
+    /* 右侧轮灌组详情 */
+    .round-group-detail {
+      flex: 1;
+      padding: 24rpx;
+
+      .group-detail-item {
+        display: none;
+
+        &.active {
+          display: block;
+        }
+
+        .group-header {
+          display: flex;
+          align-items: center;
+          justify-content: space-between;
+          margin-bottom: 24rpx;
+
+          .group-title {
+            font-size: 28rpx;
+            font-weight: 500;
+            color: #042118;
+          }
+
+          .group-check {
+            width: 32rpx;
+            height: 32rpx;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            background: #14a478;
+            color: #fff;
+            border-radius: 50%;
+            font-size: 20rpx;
+          }
+        }
+
+        .group-settings {
+          .setting-row {
+            display: flex;
+            align-items: center;
+            margin-bottom: 24rpx;
+
+            .setting-row-label {
+              width: 120rpx;
+              font-size: 26rpx;
+              color: #666;
+            }
+
+            .setting-row-input {
+              flex: 1;
+              height: 56rpx;
+              padding: 0 16rpx;
+              font-size: 26rpx;
+              border: 1rpx solid #d9d9d9;
+              border-radius: 8rpx;
+              color: #042118;
+              margin-right: 12rpx;
+            }
+
+            .setting-row-unit {
+              font-size: 24rpx;
+              color: #666;
+            }
+          }
+        }
+      }
+    }
+  }
+}
+
+/* 底部确定按钮 */
+.confirm-btn-container {
+  padding: 32rpx;
+
+  .confirm-btn {
+    height: 88rpx;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    background: #14a478;
+    border-radius: 12rpx;
+    box-shadow: 0 4rpx 12rpx rgba(20, 164, 120, 0.3);
+
+    .confirm-btn-text {
+      font-size: 32rpx;
+      font-weight: 500;
+      color: #fff;
+    }
+  }
+}
+</style>

+ 1 - 1
pages/cb/shuifeizsFirst/components/irrigatedArea.vue

@@ -23,7 +23,7 @@
             }}</view>
             <view class="irrigated-area-item-content-item-icon">
               <image
-                :src="i.value !== '0' ? bucketOpen1 : bucketClose1"
+                :src="i.value != '0' ? bucketOpen1 : bucketClose1"
                 class="bucket-icon"
               />
             </view>

+ 113 - 3
pages/cb/shuifeizsFirst/control.vue

@@ -74,12 +74,95 @@ export default {
       leftList: [],
       deviceList: [],
       devBid: '',
+      ws: null,
+      heartbeatTimer: null,
+      webSockedData: null,
+      dataArray: [],
+      info: null,
+      sfToken: '',
+      entityId: '',
     };
   },
   methods: {
+    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);
+        setTimeout(() => this.initWebSocket(), 3000);
+      });
+    },
     getChecked(item) {
       return item.value == 1 ? true : false;
     },
+    mergeTwoObject(firstObject, secondObject) {
+      for (let key in secondObject) {
+        for (let i = 0; i < firstObject.length; i++) {
+          const item = firstObject[i];
+          if (item.sfCode == key) {
+            item.value = secondObject[key][0][1];
+          } else if (item.childrenList && item.childrenList.length) {
+            this.mergeTwoObject(item.childrenList, secondObject);
+          }
+        }
+      }
+    },
     async changeSwitch(item) {
       item.value = item.value == 1 ? 0 : 1;
       const sfCode = item.sfCode;
@@ -122,14 +205,29 @@ export default {
       6	传感器	无	水肥机上的 温度,压力,流速,PH EC等监测类要素
       7	灌区    无	逻辑区域,电磁阀的分组
       */
-    async getdeviceSfStatus() {
+    async getsfrhinfo() {
       const res = await this.$myRequest({
-        url: '/api/v2/iot/mobile/device/sf/status/',
+        url: '/api/v2/iot/device/sf/info/',
         method: 'post',
         data: {
-          devBid: this.devBid,
+          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 = [
         {
@@ -159,11 +257,23 @@ export default {
       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>

+ 385 - 0
pages/cb/shuifeizsFirst/formulaSetting.vue

@@ -0,0 +1,385 @@
+<template>
+  <view class="formula-setting-page">
+    <custom-card>
+      <block slot="backText">配方设置</block>
+      <block slot="right">
+        <view class="right-icons">
+          <u-icon name="more-dot-fill" color="#333333" size="36rpx" style="margin-right: 16rpx;" />
+          <u-icon name="camera" color="#333333" size="36rpx" />
+        </view>
+      </block>
+    </custom-card>
+
+    <view class="formula-content">
+      <!-- 选择肥源 -->
+      <view class="form-section">
+        <text class="section-label">选择肥源</text>
+        <picker
+          mode="selector"
+          :range="fertilizerSources"
+          :value="selectedSourceIndex"
+          @change="onSourceChange"
+        >
+          <view class="picker-wrapper">
+            <text class="picker-text" :class="{ placeholder: selectedSourceIndex === -1 }">
+              {{ selectedSourceIndex === -1 ? '请选择肥源' : fertilizerSources[selectedSourceIndex] }}
+            </text>
+            <u-icon name="arrow-down" color="#999999" size="24rpx" />
+          </view>
+        </picker>
+      </view>
+
+      <!-- 配方名称 -->
+      <view class="form-section">
+        <text class="section-label">配方名称</text>
+        <input
+          class="formula-name-input"
+          v-model="formulaName"
+          placeholder="请输入配方名称"
+          placeholder-style="color: #999999;"
+        />
+      </view>
+
+      <!-- 肥料配置表格 -->
+      <view class="fertilizer-table">
+        <view class="table-header">
+          <text class="header-cell">肥料名称</text>
+          <text class="header-cell">EC值</text>
+          <text class="header-cell">占比</text>
+          <text class="header-cell action">操作</text>
+        </view>
+
+        <view
+          v-for="(item, index) in fertilizerList"
+          :key="index"
+          class="table-row"
+        >
+          <view class="row-cell">
+            <input
+              class="cell-input"
+              v-model="item.name"
+              placeholder="输入名称"
+              placeholder-style="color: #999999;"
+            />
+          </view>
+          <view class="row-cell">
+            <input
+              class="cell-input"
+              v-model="item.ec"
+              type="digit"
+              placeholder="EC"
+              placeholder-style="color: #999999;"
+            />
+          </view>
+          <view class="row-cell">
+            <input
+              class="cell-input"
+              v-model="item.proportion"
+              type="digit"
+              placeholder="%"
+              placeholder-style="color: #999999;"
+            />
+          </view>
+          <view class="row-cell action">
+            <text class="delete-btn" @click="deleteFertilizer(index)">删除</text>
+          </view>
+        </view>
+      </view>
+
+      <!-- 添加按钮 -->
+      <view class="add-btn-wrapper">
+        <view class="add-fertilizer-btn" @click="addFertilizer">
+          <u-icon name="plus" color="#00c853" size="28rpx" />
+          <text class="add-btn-text">添加肥料</text>
+        </view>
+      </view>
+
+      <!-- 保存按钮 -->
+      <view class="save-btn-wrapper">
+        <view class="save-btn" @click="handleSave">
+          <text class="save-text">保存</text>
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      fertilizerSources: ['肥源1', '肥源2', '肥源3'],
+      selectedSourceIndex: -1,
+      formulaName: '',
+      fertilizerList: [
+        { name: '', ec: '', proportion: '' },
+        { name: '', ec: '', proportion: '' },
+        { name: '', ec: '', proportion: '' },
+      ],
+    };
+  },
+  methods: {
+    // 肥源选择改变
+    onSourceChange(e) {
+      this.selectedSourceIndex = e.detail.value;
+    },
+
+    // 添加肥料
+    addFertilizer() {
+      if (this.fertilizerList.length >= 10) {
+        uni.showToast({
+          title: '最多添加10种肥料',
+          icon: 'none'
+        });
+        return;
+      }
+      this.fertilizerList.push({ name: '', ec: '', proportion: '' });
+    },
+
+    // 删除肥料
+    deleteFertilizer(index) {
+      if (this.fertilizerList.length <= 1) {
+        uni.showToast({
+          title: '至少保留一种肥料',
+          icon: 'none'
+        });
+        return;
+      }
+      uni.showModal({
+        title: '确认删除',
+        content: '确定要删除该肥料吗?',
+        success: (res) => {
+          if (res.confirm) {
+            this.fertilizerList.splice(index, 1);
+          }
+        }
+      });
+    },
+
+    // 保存
+    handleSave() {
+      // 验证
+      if (this.selectedSourceIndex === -1) {
+        uni.showToast({
+          title: '请选择肥源',
+          icon: 'none'
+        });
+        return;
+      }
+
+      if (!this.formulaName.trim()) {
+        uni.showToast({
+          title: '请输入配方名称',
+          icon: 'none'
+        });
+        return;
+      }
+
+      // 验证肥料列表
+      const validFertilizers = this.fertilizerList.filter(item => {
+        return item.name.trim() || item.ec || item.proportion;
+      });
+
+      if (validFertilizers.length === 0) {
+        uni.showToast({
+          title: '请至少添加一种肥料',
+          icon: 'none'
+        });
+        return;
+      }
+
+      // 构建保存数据
+      const saveData = {
+        fertilizerSource: this.fertilizerSources[this.selectedSourceIndex],
+        formulaName: this.formulaName,
+        fertilizers: validFertilizers
+      };
+
+      console.log('保存配方设置:', saveData);
+
+      // TODO: 调用接口保存
+
+      uni.showToast({
+        title: '保存成功',
+        icon: 'success'
+      });
+    },
+  },
+};
+</script>
+
+<style scoped lang="scss">
+uni-page-body {
+  position: relative;
+  height: 100%;
+}
+
+.formula-setting-page {
+  min-height: 100%;
+  background: linear-gradient(180deg, #e6f7f0 0%, #ffffff 100%);
+  overflow-x: hidden;
+  overflow-y: auto;
+
+  .right-icons {
+    display: flex;
+    align-items: center;
+  }
+
+  .formula-content {
+    padding: 24rpx 32rpx;
+  }
+
+  // 表单区域
+  .form-section {
+    background: #ffffff;
+    border-radius: 12rpx;
+    padding: 24rpx;
+    margin-bottom: 20rpx;
+
+    .section-label {
+      display: block;
+      font-size: 28rpx;
+      font-weight: 500;
+      color: #333333;
+      margin-bottom: 20rpx;
+      font-family: 'Source Han Sans CN';
+    }
+
+    .picker-wrapper {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      padding: 20rpx 24rpx;
+      background: #f5f5f5;
+      border-radius: 8rpx;
+
+      .picker-text {
+        font-size: 28rpx;
+        color: #333333;
+        font-family: 'Source Han Sans CN';
+
+        &.placeholder {
+          color: #999999;
+        }
+      }
+    }
+
+    .formula-name-input {
+      width: 100%;
+      padding: 20rpx 24rpx;
+      background: #f5f5f5;
+      border-radius: 8rpx;
+      font-size: 28rpx;
+      color: #333333;
+      font-family: 'Source Han Sans CN';
+    }
+  }
+
+  // 肥料表格
+  .fertilizer-table {
+    background: #ffffff;
+    border-radius: 12rpx;
+    padding: 24rpx;
+    margin-bottom: 20rpx;
+
+    .table-header {
+      display: flex;
+      padding-bottom: 20rpx;
+      border-bottom: 1rpx solid #f0f0f0;
+
+      .header-cell {
+        flex: 1;
+        font-size: 26rpx;
+        font-weight: 500;
+        color: #666666;
+        text-align: center;
+        font-family: 'Source Han Sans CN';
+
+        &.action {
+          flex: 0.8;
+        }
+      }
+    }
+
+    .table-row {
+      display: flex;
+      padding: 24rpx 0;
+      border-bottom: 1rpx solid #f0f0f0;
+
+      &:last-child {
+        border-bottom: none;
+      }
+
+      .row-cell {
+        flex: 1;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+
+        .cell-input {
+          width: 100%;
+          padding: 12rpx 16rpx;
+          background: #f5f5f5;
+          border-radius: 6rpx;
+          font-size: 26rpx;
+          color: #333333;
+          text-align: center;
+          font-family: 'Source Han Sans CN';
+        }
+
+        &.action {
+          flex: 0.8;
+
+          .delete-btn {
+            font-size: 26rpx;
+            color: #ff4d4f;
+            font-family: 'Source Han Sans CN';
+          }
+        }
+      }
+    }
+  }
+
+  // 添加按钮区域
+  .add-btn-wrapper {
+    margin-bottom: 20rpx;
+
+    .add-fertilizer-btn {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      height: 80rpx;
+      background: #ffffff;
+      border: 2rpx dashed #00c853;
+      border-radius: 12rpx;
+
+      .add-btn-text {
+        margin-left: 8rpx;
+        font-size: 28rpx;
+        color: #00c853;
+        font-family: 'Source Han Sans CN';
+      }
+    }
+  }
+
+  // 保存按钮
+  .save-btn-wrapper {
+    .save-btn {
+      width: 100%;
+      height: 88rpx;
+      background: #00c853;
+      border-radius: 12rpx;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+
+      .save-text {
+        font-size: 32rpx;
+        font-weight: 500;
+        color: #ffffff;
+        font-family: 'Source Han Sans CN';
+      }
+    }
+  }
+}
+</style>

+ 138 - 22
pages/cb/shuifeizsFirst/shuifeizs.vue

@@ -26,6 +26,7 @@
       :ggbCurrent="ggbCurrent"
       :sfbCurrent="sfbCurrent"
       :irrigatedAreaList="irrigatedAreaList"
+      :webSockedData="webSockedData"
       :alreadyfertilizerBucketList="alreadyfertilizerBucketList"
     />
   </view>
@@ -45,6 +46,7 @@ import facilitystate from './components/facilitystate.vue';
 export default {
   data() {
     return {
+      ws: null,
       title: '水肥一体机',
       manualControl,
       devBid: '',
@@ -59,21 +61,25 @@ export default {
           title: '手动控制',
           url: '/pages/cb/shuifeizsFirst/control',
         },
-        // {
-        //   icon: wheelIrrigation,
-        //   title: '自动控制',
-        //   url: '',
-        // },
+        {
+          icon: wheelIrrigation,
+          title: '自动控制',
+          url: '/pages/cb/shuifeizsFirst/autoSetting',
+        }, {
+          icon: timing,
+          title: '定时设置',
+          url: '/pages/cb/shuifeizsFirst/timingSetting',
+        },{
+          icon: timing,
+          title: '配方设置',
+          url: '/pages/cb/shuifeizsFirst/formulaSetting',
+        },
         {
           icon: operatingRecord,
           title: '操作记录',
           url: '/pages/cb/shuifeizsFirst/history',
         },
-        // {
-        //   icon: timing,
-        //   title: '定时计划',
-        //   url: '',
-        // },
+       
         // {
         //   icon: masterStop,
         //   title: '总停',
@@ -97,6 +103,12 @@ export default {
       ],
       ggbCurrent: {},
       sfbCurrent: {},
+      sfToken: '',
+      entityId: '',
+      heartbeatTimer: null,
+      info: {},
+      dataArray:[],
+      webSockedData: {},
     };
   },
   components: {
@@ -104,7 +116,6 @@ export default {
     facilitystate,
   },
   onLoad(options) {
-    console.log(options, 'optionsoptions');
     /*
       0	水源	泵类	负责水源进入管道的总泵或者阀
       1	肥料	泵类	负责肥料进入管道的总阀或者泵
@@ -127,15 +138,117 @@ export default {
       this.init();
     }
   },
+  onUnload() {
+    this.ws.close();
+  },
   methods: {
+    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);
+            }
+            // this.$refs.facilRightItem && this.$refs.facilRightItem.init();
+        } catch (e) {
+          console.error('WebSocket 消息解析失败', e);
+        }
+      });
+
+      uni.onSocketError((e) => {
+        console.error('WebSocket 错误', e);
+      });
+
+      uni.onSocketClose(() => {
+        console.warn('WebSocket 已断开,3 秒后尝试重连');
+        clearInterval(this.heartbeatTimer);
+        setTimeout(() => this.initWebSocket(), 3000);
+      });
+    },
+    mergeTwoObject(firstObject, secondObject) {
+      for (let key in secondObject) {
+        for (let i = 0; i < firstObject.length; i++) {
+          const item = firstObject[i];
+          if (item.sfCode == key) {
+            item.value = secondObject[key][0][1];
+          } else if (item.childrenList && item.childrenList.length) {
+            this.mergeTwoObject(item.childrenList, secondObject);
+          }
+        }
+      }
+    },
+    // 关闭 WebSocket
+    closeWebSocket() {
+      clearInterval(this.heartbeatTimer);
+      if (this.ws) {
+        uni.closeSocket();
+        this.ws = null;
+      }
+    },
+    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();
+    },
     async init() {
       this.getdeviceSfStatus();
       await this.getpeifangRefresh();
+      this.getsfrhinfo();
       await this.getRunStatus();
     },
-    // iot/mobile/device/sf/peifang/refresh/
     async getpeifangRefresh() {
-      const res = await this.$myRequest({
+      await this.$myRequest({
         url: '/api/v2/iot/mobile/device/sf/peifang/refresh/',
         method: 'post',
         data: {
@@ -161,14 +274,7 @@ export default {
         // this.deviceList[1].url = `/pages/cb/shuifeizsFirst/rotationflow?devBid=${this.devBid}&devName=${this.devName}&devStatus=${this.devStatus}`;
       }
     },
-    async getdeviceSfStatus() {
-      const res = await this.$myRequest({
-        url: '/api/v2/iot/mobile/device/sf/status/',
-        method: 'post',
-        data: {
-          devBid: this.devBid,
-        },
-      });
+    initData(res){
       this.dataList = [];
       this.irrigatedAreaList = [];
       this.alreadyfertilizerBucketList = [];
@@ -185,7 +291,17 @@ export default {
           this.irrigatedAreaList.push(item);
         }
       });
-      console.log(this.alreadyfertilizerBucketList,'alreadyfertilizerBucketListalreadyfertilizerBucketList')
+    },
+    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(this.dataArray);
     },
     nativeTo(item) {
       uni.navigateTo({

+ 271 - 0
pages/cb/shuifeizsFirst/timingSetting.vue

@@ -0,0 +1,271 @@
+<template>
+  <view class="timing-setting-page">
+    <custom-card>
+      <block slot="backText">定时设置</block>
+      <block slot="right">
+        <view class="right-icons">
+          <u-icon name="more-dot-fill" color="#333333" size="36rpx" style="margin-right: 16rpx;" />
+          <u-icon name="camera" color="#333333" size="36rpx" />
+        </view>
+      </block>
+    </custom-card>
+
+    <view class="timing-content">
+      <!-- 定时卡片列表 -->
+      <view
+        class="timer-card"
+        v-for="(timer, index) in timerList"
+        :key="index"
+      >
+        <!-- 定时标题和开关 -->
+        <view class="timer-header">
+          <text class="timer-title">定时{{ index + 1 }}</text>
+          <switch
+            :checked="timer.enabled"
+            @change="onSwitchChange($event, index)"
+            color="#00c853"
+            class="timer-switch"
+          />
+        </view>
+
+        <!-- 单选按钮组 -->
+        <view class="radio-group">
+          <view
+            class="radio-item"
+            :class="{ active: timer.frequency === 'once' }"
+            @click="selectFrequency(index, 'once')"
+          >
+            <view class="radio-icon">
+              <view v-if="timer.frequency === 'once'" class="radio-dot"></view>
+            </view>
+            <text class="radio-text">仅一次</text>
+          </view>
+          <view
+            class="radio-item"
+            :class="{ active: timer.frequency === 'daily' }"
+            @click="selectFrequency(index, 'daily')"
+          >
+            <view class="radio-icon">
+              <view v-if="timer.frequency === 'daily'" class="radio-dot"></view>
+            </view>
+            <text class="radio-text">每天一次</text>
+          </view>
+        </view>
+
+        <!-- 时间选择器 -->
+        <picker
+          mode="time"
+          :value="timer.time"
+          @change="onTimeChange($event, index)"
+        >
+          <view class="time-picker">
+            <text class="time-text" :class="{ placeholder: !timer.time }">
+              {{ timer.time || '请选择时间' }}
+            </text>
+            <u-icon name="clock" color="#999999" size="32rpx" />
+          </view>
+        </picker>
+      </view>
+
+      <!-- 确定按钮 -->
+      <view class="confirm-btn" @click="handleConfirm">
+        <text class="confirm-text">确定</text>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      timerList: [
+        { enabled: false, frequency: 'once', time: '' },
+        { enabled: false, frequency: 'once', time: '' },
+        { enabled: false, frequency: 'once', time: '' },
+        { enabled: false, frequency: 'once', time: '' },
+        { enabled: false, frequency: 'once', time: '' },
+      ],
+    };
+  },
+  methods: {
+    // 开关状态改变
+    onSwitchChange(e, index) {
+      this.timerList[index].enabled = e.detail.value;
+    },
+
+    // 选择频率
+    selectFrequency(index, frequency) {
+      this.timerList[index].frequency = frequency;
+    },
+
+    // 时间改变
+    onTimeChange(e, index) {
+      this.timerList[index].time = e.detail.value;
+    },
+
+    // 确认按钮
+    handleConfirm() {
+      const enabledTimers = this.timerList.filter((timer, index) => {
+        if (timer.enabled) {
+          if (!timer.time) {
+            uni.showToast({
+              title: `定时${index + 1}请选择时间`,
+              icon: 'none'
+            });
+            return false;
+          }
+          return true;
+        }
+        return false;
+      });
+
+      console.log('保存的定时设置:', enabledTimers);
+
+      uni.showToast({
+        title: '保存成功',
+        icon: 'success'
+      });
+
+      // TODO: 调用接口保存定时设置
+    },
+  },
+};
+</script>
+
+<style scoped lang="scss">
+uni-page-body {
+  position: relative;
+  height: 100%;
+}
+
+.timing-setting-page {
+  min-height: 100%;
+  background: linear-gradient(180deg, #e6f7f0 0%, #ffffff 100%);
+  overflow-x: hidden;
+  overflow-y: auto;
+
+  .right-icons {
+    display: flex;
+    align-items: center;
+  }
+
+  .timing-content {
+    padding: 24rpx 32rpx;
+  }
+
+  // 定时卡片
+  .timer-card {
+    background: #ffffff;
+    border-radius: 12rpx;
+    padding: 24rpx;
+    margin-bottom: 20rpx;
+
+    // 卡片头部
+    .timer-header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      margin-bottom: 24rpx;
+
+      .timer-title {
+        font-size: 32rpx;
+        font-weight: 600;
+        color: #333333;
+        font-family: 'Source Han Sans CN';
+      }
+
+      .timer-switch {
+        transform: scale(0.9);
+      }
+    }
+
+    // 单选按钮组
+    .radio-group {
+      display: flex;
+      margin-bottom: 24rpx;
+
+      .radio-item {
+        display: flex;
+        align-items: center;
+        margin-right: 48rpx;
+        cursor: pointer;
+
+        .radio-icon {
+          width: 36rpx;
+          height: 36rpx;
+          border: 4rpx solid #cccccc;
+          border-radius: 50%;
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          margin-right: 12rpx;
+          box-sizing: border-box;
+
+          .radio-dot {
+            width: 18rpx;
+            height: 18rpx;
+            background: #00c853;
+            border-radius: 50%;
+          }
+        }
+
+        &.active {
+          .radio-icon {
+            border-color: #00c853;
+          }
+
+          .radio-text {
+            color: #333333;
+          }
+        }
+
+        .radio-text {
+          font-size: 28rpx;
+          color: #999999;
+          font-family: 'Source Han Sans CN';
+        }
+      }
+    }
+
+    // 时间选择器
+    .time-picker {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      padding: 20rpx 24rpx;
+      background: #f5f5f5;
+      border-radius: 8rpx;
+
+      .time-text {
+        font-size: 28rpx;
+        color: #333333;
+        font-family: 'Source Han Sans CN';
+
+        &.placeholder {
+          color: #999999;
+        }
+      }
+    }
+  }
+
+  // 确定按钮
+  .confirm-btn {
+    width: 100%;
+    height: 88rpx;
+    background: #00c853;
+    border-radius: 12rpx;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    margin-top: 20rpx;
+
+    .confirm-text {
+      font-size: 32rpx;
+      font-weight: 500;
+      color: #ffffff;
+      font-family: 'Source Han Sans CN';
+    }
+  }
+}
+</style>

+ 2 - 2
util/api.js

@@ -1,10 +1,10 @@
 // let BASE_URL = 'http://114.55.0.7:8002';
 // const BASE_URL='http://8.136.98.49:8002'
-// let BASE_URL = 'http://218.28.198.186:10505'
+let BASE_URL = 'http://218.28.198.186:10505'
 // let BASE_URL = 'https://wx.hnyfwlw.com'
 // let BASE_URL = 'http://192.168.1.107:8000';
 // let BASE_URL = 'http://218.28.198.186:10508';
-let BASE_URL = 'https://uat.hnyfwlw.com'
+// let BASE_URL = 'https://uat.hnyfwlw.com'
 export const myRequest = (options) => {
   // BASE_URL=uni.getStorageSync('http')
   // if(BASE_URL==''){