소스 검색

feat(views): 实时监测页面支持智能启停和寻优

wangcong 2 주 전
부모
커밋
61ec60a92f

+ 148 - 43
src/views/real-time-monitor/device-control/AIOptimization.vue

@@ -1,53 +1,99 @@
 <script setup lang="ts">
-import { computed, ref, shallowRef } from 'vue';
-import { message, Modal } from 'ant-design-vue';
+import { computed, onMounted, ref, shallowRef, useTemplateRef, watch } from 'vue';
+import { Modal } from 'ant-design-vue';
 
+import { DeviceType } from '@/views/device-work-status/device-card';
 import SvgIcon from '@/components/SvgIcon.vue';
+import { useRequest } from '@/hooks/request';
 import { useViewVisible } from '@/hooks/view-visible';
 import { t } from '@/i18n';
+import { getAlgorithmConfigInfo, updateAIStartStopOptimize } from '@/api';
+import { isNotEmptyVal } from '@/utils';
 
 import ChillerUnitOptimize from '../device-optimize/ChillerUnitOptimize.vue';
 import CoolingTowerOptimize from '../device-optimize/CoolingTowerOptimize.vue';
 
 import type { Component } from 'vue';
 import type { ColumnType } from 'ant-design-vue/es/table';
+import type { AIOptimizeSetItemInstance, GroupModuleInfo } from '@/types';
+
+interface Props {
+  info: GroupModuleInfo;
+}
+
+const props = defineProps<Props>();
 
 interface OptimizeAlgorithmItem {
   algorithm: string;
   setValue: number;
   isEnabled: boolean;
+  valuePropName: string;
+  enablePropName: string;
   unit?: string;
+  deviceType: DeviceType;
   modalWidth?: number;
   modalComponent?: Component;
 }
 
-const dataSource = ref<OptimizeAlgorithmItem[]>([
-  {
-    algorithm: '冷机水温寻优',
-    setValue: 10,
-    isEnabled: false,
-    unit: '°C',
-    modalComponent: shallowRef(ChillerUnitOptimize),
-  },
-  {
-    algorithm: '塔出水温度寻优',
-    setValue: 10,
-    isEnabled: true,
-    unit: '°C',
-    modalWidth: 460,
-    modalComponent: shallowRef(CoolingTowerOptimize),
-  },
-  {
-    algorithm: '冷冻泵寻优',
-    setValue: 10,
-    isEnabled: true,
-  },
-  {
-    algorithm: '冷却泵寻优',
-    setValue: 10,
-    isEnabled: true,
+const { handleRequest } = useRequest();
+const chillerUnitWaterTemp = ref(props.info.moduleInfoAi.aiSeekHostWaterTempValue);
+const coolingTowerWaterTemp = ref(props.info.moduleInfoAi.aiSeekTowerWaterTempValue);
+
+watch(
+  () => props.info.moduleInfoAi,
+  (newVal) => {
+    chillerUnitWaterTemp.value = newVal.aiSeekHostWaterTempValue;
+    coolingTowerWaterTemp.value = newVal.aiSeekTowerWaterTempValue;
   },
-]);
+);
+
+const enableChillerUnit = ref(false);
+const enableCoolingTower = ref(false);
+
+const dataSource = computed<OptimizeAlgorithmItem[]>(() => {
+  const list: OptimizeAlgorithmItem[] = [];
+  const { moduleInfoAi } = props.info;
+
+  if (enableChillerUnit.value) {
+    list.push({
+      algorithm: '冷机水温寻优',
+      setValue: moduleInfoAi.aiSeekHostWaterTempValue,
+      isEnabled: moduleInfoAi.aiSeekHostWaterTempButton,
+      valuePropName: 'aiSeekHostWaterTempValue',
+      enablePropName: 'aiSeekHostWaterTempButton',
+      unit: '°C',
+      deviceType: DeviceType.冷水主机,
+      modalComponent: shallowRef(ChillerUnitOptimize),
+    });
+  }
+
+  if (enableCoolingTower.value) {
+    list.push({
+      algorithm: '塔出水温度寻优',
+      setValue: moduleInfoAi.aiSeekTowerWaterTempValue,
+      isEnabled: moduleInfoAi.aiSeekTowerWaterTempButton,
+      valuePropName: 'aiSeekTowerWaterTempValue',
+      enablePropName: 'aiSeekTowerWaterTempButton',
+      unit: '°C',
+      deviceType: DeviceType.冷却塔,
+      modalWidth: 460,
+      modalComponent: shallowRef(CoolingTowerOptimize),
+    });
+  }
+
+  // {
+  //   algorithm: '冷冻泵寻优',
+  //   setValue: 10,
+  //   isEnabled: true,
+  // },
+  // {
+  //   algorithm: '冷却泵寻优',
+  //   setValue: 10,
+  //   isEnabled: true,
+  // },
+
+  return list;
+});
 
 const columns = computed<ColumnType<OptimizeAlgorithmItem>[]>(() => [
   {
@@ -72,19 +118,48 @@ const columns = computed<ColumnType<OptimizeAlgorithmItem>[]>(() => [
   },
 ]);
 
+onMounted(() => {
+  handleRequest(async () => {
+    const { groupId } = props.info;
+    const { enabled, enableCoolingPipeDynamicSet } = await getAlgorithmConfigInfo(groupId);
+    enableChillerUnit.value = enabled;
+    enableCoolingTower.value = enableCoolingPipeDynamicSet;
+  });
+});
+
+const handleTempChange = (record: OptimizeAlgorithmItem, value: string | number | null) => {
+  handleRequest(async () => {
+    const id = props.info.moduleInfoAi.id;
+    const groupId = props.info.groupId;
+
+    if (isNotEmptyVal(value)) {
+      await updateAIStartStopOptimize({
+        id,
+        groupId,
+        [record.valuePropName]: value,
+      });
+    }
+  });
+};
+
 const handleSwitchClick = (record: OptimizeAlgorithmItem) => {
   const titlePrefix = record.isEnabled ? t('common.confirmClose') : t('common.confirmOpen');
 
   Modal.confirm({
     title: titlePrefix + record.algorithm,
     closable: true,
-    async onOk() {
-      try {
-        record.isEnabled = !record.isEnabled;
-      } catch (err) {
-        message.error((err as Error).message);
-        console.error(err);
-      }
+    centered: true,
+    onOk() {
+      handleRequest(async () => {
+        const id = props.info.moduleInfoAi.id;
+        const groupId = props.info.groupId;
+
+        await updateAIStartStopOptimize({
+          id,
+          groupId,
+          [record.enablePropName]: !record.isEnabled,
+        });
+      });
     },
   });
 };
@@ -96,14 +171,19 @@ const handleSettingClick = (record: OptimizeAlgorithmItem) => {
   showView();
 };
 
-const { visible, showView } = useViewVisible();
+const { visible, showView, hideView } = useViewVisible();
+const { isLoading: isOptimizeSetLoading, handleRequest: handleOptimizeSetRequest } = useRequest();
+const optimizeSetRef = useTemplateRef<AIOptimizeSetItemInstance>('optimizeSetItem');
 
 const modalTitle = computed(() => {
   return selectedAlgorithm.value?.algorithm + t('common.settings');
 });
 
 const handleOk = () => {
-  // 实际场景中处理确定逻辑
+  handleOptimizeSetRequest(async () => {
+    await optimizeSetRef.value?.submit?.();
+    hideView();
+  });
 };
 </script>
 
@@ -116,7 +196,22 @@ const handleOk = () => {
           <span v-if="record.unit">({{ record.unit }})</span>
         </template>
         <template v-if="column.key === 'setValue'">
-          <AInput class="ai-optimize-input" v-model:value="record.setValue" />
+          <AInputNumber
+            v-if="record.algorithm === '冷机水温寻优'"
+            class="ai-optimize-input"
+            v-model:value="chillerUnitWaterTemp"
+            :disabled="record.isEnabled"
+            :controls="false"
+            @change="handleTempChange(record as OptimizeAlgorithmItem, $event)"
+          />
+          <AInputNumber
+            v-else-if="record.algorithm === '塔出水温度寻优'"
+            class="ai-optimize-input"
+            v-model:value="coolingTowerWaterTemp"
+            :disabled="record.isEnabled"
+            :controls="false"
+            @change="handleTempChange(record as OptimizeAlgorithmItem, $event)"
+          />
         </template>
         <template v-if="column.key === 'isEnabled'">
           <ASwitch
@@ -138,10 +233,12 @@ const handleOk = () => {
       wrap-class-name="hvac-modal ai-optimize-modal"
       :title="modalTitle"
       :width="selectedAlgorithm?.modalWidth || 920"
+      centered
       destroy-on-close
+      :confirm-loading="isOptimizeSetLoading"
       @ok="handleOk"
     >
-      <component :is="selectedAlgorithm?.modalComponent" />
+      <component ref="optimizeSetItem" :is="selectedAlgorithm?.modalComponent" :group-id="info.groupId" />
     </AModal>
   </div>
 </template>
@@ -214,16 +311,24 @@ const handleOk = () => {
 
 .ai-optimize-input {
   width: 96px;
-  color: #fff;
   background: rgb(30 37 48 / 32%);
   border-color: rgb(255 255 255 / 24%);
   border-radius: 4px;
 
-  &.ant-input:hover,
-  &.ant-input:focus,
-  &.ant-input-focused {
+  :deep(.ant-input-number-input) {
+    color: #fff;
+  }
+
+  &:hover,
+  &:focus,
+  &.ant-input-number-focused {
     border-color: var(--antd-color-primary-hover);
   }
+
+  &.ant-input-number-disabled {
+    border-color: rgb(255 255 255 / 24%);
+    opacity: 0.4;
+  }
 }
 
 .ai-optimize-setting {

+ 113 - 46
src/views/real-time-monitor/device-control/AIStartStop.vue

@@ -1,16 +1,24 @@
 <script setup lang="ts">
-import { computed, ref } from 'vue';
-import { message, Modal } from 'ant-design-vue';
+import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
+import { Modal } from 'ant-design-vue';
 
 import SvgIcon from '@/components/SvgIcon.vue';
+import { useRequest } from '@/hooks/request';
 import { t } from '@/i18n';
+import { getAIStartStopData, updateAIStartStopData, updateAIStartStopOptimize } from '@/api';
+import { CtrlCabinetStartType } from '@/constants';
 
 import type { SegmentedBaseOption } from 'ant-design-vue/es/segmented/src/segmented';
+import type { AIStartStopDeviceItem, GroupModuleInfo } from '@/types';
 
-type ModeValue = 'fullAuto' | 'halfAuto' | 'jog';
+interface Props {
+  info: GroupModuleInfo;
+}
+
+const props = defineProps<Props>();
 
 interface ModeTypeItem extends SegmentedBaseOption {
-  value: ModeValue;
+  value: CtrlCabinetStartType;
   payload: {
     title: string;
     confirmTip: string;
@@ -20,21 +28,21 @@ interface ModeTypeItem extends SegmentedBaseOption {
 const modeTypes = computed<ModeTypeItem[]>(() => {
   return [
     {
-      value: 'fullAuto',
+      value: CtrlCabinetStartType.FullAuto,
       payload: {
         title: t('realTimeMonitor.fullAutoMode'),
         confirmTip: t('realTimeMonitor.fullAutoConfirmTip'),
       },
     },
     {
-      value: 'halfAuto',
+      value: CtrlCabinetStartType.HalfAuto,
       payload: {
         title: t('realTimeMonitor.halfAutoMode'),
         confirmTip: t('realTimeMonitor.halfAutoConfirmTip'),
       },
     },
     {
-      value: 'jog',
+      value: CtrlCabinetStartType.Jog,
       disabled: true,
       payload: {
         title: t('realTimeMonitor.jogMode'),
@@ -44,7 +52,14 @@ const modeTypes = computed<ModeTypeItem[]>(() => {
   ];
 });
 
-const currentModeType = ref<ModeValue>('fullAuto');
+const currentModeType = computed<CtrlCabinetStartType>(() => {
+  return props.info.moduleInfoAi?.aiStartType ?? CtrlCabinetStartType.FullAuto;
+});
+
+watch(currentModeType, () => {
+  deviceList.value = [];
+  getDeviceList();
+});
 
 const handleModeClick = (option: ModeTypeItem) => {
   if (option.disabled || option.value === currentModeType.value) {
@@ -55,40 +70,89 @@ const handleModeClick = (option: ModeTypeItem) => {
     title: t('realTimeMonitor.confirmSwitchToMode', { mode: option.payload.title }),
     content: option.payload.confirmTip,
     closable: true,
-    async onOk() {
-      try {
-        currentModeType.value = option.value;
-      } catch (err) {
-        message.error((err as Error).message);
-        console.error(err);
-      }
+    centered: true,
+    onOk() {
+      handleRequest(async () => {
+        const id = props.info.moduleInfoAi.id;
+        const groupId = props.info.groupId;
+
+        await updateAIStartStopOptimize({
+          id,
+          groupId,
+          aiStartType: option.value,
+        });
+      });
     },
   });
 };
 
-type DeviceStatus = 'on' | 'off';
+const { handleRequest } = useRequest();
+const deviceList = ref<AIStartStopDeviceItem[]>([]);
+let deviceTimer: number | undefined;
 
-interface DeviceItem {
-  name: string;
-  status: DeviceStatus;
-}
+onMounted(() => {
+  getDeviceList();
+});
+
+onUnmounted(() => {
+  deviceList.value = [];
+  clearTimeout(deviceTimer);
+});
+
+const getDeviceList = () => {
+  clearTimeout(deviceTimer);
+
+  handleRequest(async () => {
+    const groupId = props.info.groupId;
+    const startType = props.info.moduleInfoAi.aiStartType;
+    deviceList.value = await getAIStartStopData(groupId, startType);
+  });
+
+  deviceTimer = window.setTimeout(getDeviceList, 3000);
+};
+
+const handleFullStartStatusSwitch = () => {
+  const device = deviceList.value[0];
+  const title = device.startStatus ? t('realTimeMonitor.confirmFullPowerOff') : t('realTimeMonitor.confirmFullPowerOn');
+  const content = device.startStatus
+    ? t('realTimeMonitor.confirmFullPowerOffTip')
+    : t('realTimeMonitor.confirmFullPowerOnTip');
+
+  Modal.confirm({
+    title,
+    content,
+    closable: true,
+    centered: true,
+    onOk() {
+      handleRequest(async () => {
+        await updateAIStartStopData({
+          deviceId: device.deviceId,
+          status: !device.startStatus,
+          startType: currentModeType.value,
+        });
+      });
+    },
+  });
+};
+
+const handleStartStatusSwitch = (device: AIStartStopDeviceItem) => {
+  const title = device.startStatus ? t('common.confirmClose') : t('common.confirmOpen');
 
-const deviceList = ref<DeviceItem[]>([
-  { name: '五期-1#冷水主机', status: 'off' },
-  { name: '五期-2#冷水主机', status: 'on' },
-  { name: '五期-3#冷水主机', status: 'off' },
-  { name: '五期-3#冷水主机', status: 'off' },
-  { name: '五期-3#冷水主机', status: 'off' },
-  { name: '五期-3#冷水主机', status: 'off' },
-  { name: '五期-3#冷水主机', status: 'off' },
-  { name: '五期-3#冷水主机', status: 'off' },
-  { name: '五期-3#冷水主机', status: 'off' },
-  { name: '五期-3#冷水主机', status: 'off' },
-  { name: '五期-3#冷水主机', status: 'off' },
-  { name: '五期-3#冷水主机', status: 'off' },
-  { name: '五期-3#冷水主机', status: 'off' },
-  { name: '五期-3#冷水主机', status: 'off' },
-]);
+  Modal.confirm({
+    title: title + device.deviceName,
+    closable: true,
+    centered: true,
+    onOk() {
+      handleRequest(async () => {
+        await updateAIStartStopData({
+          deviceId: device.deviceId,
+          status: !device.startStatus,
+          startType: currentModeType.value,
+        });
+      });
+    },
+  });
+};
 </script>
 
 <template>
@@ -98,29 +162,32 @@ const deviceList = ref<DeviceItem[]>([
         <span @click="handleModeClick(option as ModeTypeItem)">{{ option.payload.title }}</span>
       </template>
     </ASegmented>
-    <template v-if="currentModeType === 'fullAuto'">
-      <div class="power-button">
+    <template v-if="currentModeType === CtrlCabinetStartType.FullAuto">
+      <div class="power-button" @click="handleFullStartStatusSwitch">
         <div class="power-button-content">
           <SvgIcon name="power-off" />
-          <div>{{ $t('realTimeMonitor.fullPowerOff') }}</div>
+          <div>
+            {{ deviceList[0]?.startStatus ? $t('realTimeMonitor.fullPowerOff') : $t('realTimeMonitor.fullPowerOn') }}
+          </div>
         </div>
         <div class="power-button-segmented-border"></div>
       </div>
     </template>
-    <div v-else-if="currentModeType === 'halfAuto'" class="ctrl-panel-scroll-content">
+    <div v-else-if="currentModeType === CtrlCabinetStartType.HalfAuto" class="ctrl-panel-scroll-content">
       <div v-for="(item, index) in deviceList" :key="index" class="device-item">
         <div>
-          <span :class="['device-item-tag', { 'device-tag-run': item.status === 'on' }]">
-            {{ item.status === 'on' ? $t('common.run') : $t('common.shutDown') }}
+          <span :class="['device-item-tag', { 'device-tag-run': item.runningStatus === 1 }]">
+            {{ item.runningStatus === 1 ? $t('common.run') : $t('common.shutDown') }}
           </span>
-          <span class="device-item-title">{{ item.name }}</span>
+          <span class="device-item-title">{{ item.deviceName }}</span>
         </div>
         <ASwitch
-          v-model:checked="item.status"
-          checked-value="on"
-          un-checked-value="off"
+          :checked="item.startStatus"
+          :checked-value="1"
+          :un-checked-value="0"
           :checked-children="$t('common.on')"
           :un-checked-children="$t('common.off')"
+          @click="handleStartStatusSwitch(item)"
         />
       </div>
     </div>

+ 41 - 2
src/views/real-time-monitor/device-control/DeviceControl.vue

@@ -1,14 +1,41 @@
 <script setup lang="ts">
-import { computed, ref } from 'vue';
+import { computed, onUnmounted, ref, watch } from 'vue';
 
+import { useRequest } from '@/hooks/request';
 import { t } from '@/i18n';
+import { getGroupModuleInfo } from '@/api';
 import { addUnit } from '@/utils';
+import { VisualModuleType } from '@/constants';
 
 import AdvancedSettings from './AdvancedSettings.vue';
 import AIOptimization from './AIOptimization.vue';
 import AIStartStop from './AIStartStop.vue';
 
 import type { Component, CSSProperties } from 'vue';
+import type { GroupModuleInfo } from '@/types';
+
+interface Props {
+  info: GroupModuleInfo;
+}
+
+const props = defineProps<Props>();
+
+const { handleRequest } = useRequest();
+const moduleInfo = ref<GroupModuleInfo>(props.info);
+let moduleTimer: number | undefined;
+
+const getModuleInfo = () => {
+  clearTimeout(moduleTimer);
+
+  handleRequest(async () => {
+    moduleInfo.value = await getGroupModuleInfo({
+      groupId: props.info.groupId,
+      moduleType: VisualModuleType.Module2D,
+    });
+  });
+
+  moduleTimer = window.setTimeout(getModuleInfo, 3000);
+};
 
 interface ConfigItem {
   label: string;
@@ -36,6 +63,14 @@ const showCtrlPanel = computed(() => {
   return activeConfigIndex.value !== -1;
 });
 
+watch(activeConfigIndex, () => {
+  if (activeConfigIndex.value === -1) {
+    clearTimeout(moduleTimer);
+  } else {
+    getModuleInfo();
+  }
+});
+
 const toggleConfig = (index: number) => {
   if (activeConfigIndex.value === index) {
     closeCtrlPanel();
@@ -48,6 +83,10 @@ const closeCtrlPanel = () => {
   activeConfigIndex.value = -1;
 };
 
+onUnmounted(() => {
+  clearTimeout(moduleTimer);
+});
+
 defineExpose({
   closeCtrlPanel,
 });
@@ -70,7 +109,7 @@ defineExpose({
     </div>
     <div class="ctrl-panel" v-if="showCtrlPanel">
       <div class="ctrl-panel-title">{{ configs[activeConfigIndex].label }}</div>
-      <component :is="configs[activeConfigIndex].component" />
+      <component :is="configs[activeConfigIndex].component" :info="moduleInfo" />
     </div>
   </div>
 </template>

+ 89 - 44
src/views/real-time-monitor/device-optimize/ChillerUnitOptimize.vue

@@ -1,25 +1,25 @@
 <script setup lang="ts">
-import { computed, ref } from 'vue';
+import { computed, onMounted, ref } from 'vue';
 import dayjs from 'dayjs';
 
+import { DeviceType } from '@/views/device-work-status/device-card';
+import { useRequest } from '@/hooks/request';
 import { t } from '@/i18n';
+import { getAIOptimizeDetail, updateAIOptimizeSetting } from '@/api';
+import { isEmptyVal } from '@/utils';
 
 import type { ColumnType } from 'ant-design-vue/es/table';
-import type { RangeValue } from '@/types';
+import type { AIOptimizeDeviceItem, AIOptimizeSetItemExpose, TemperatureRange, TemperatureRangeItem } from '@/types';
 
-interface DeviceAlgorithmItem {
-  deviceName: string;
-  deviceStatus: 'on' | 'off';
-  isInAlgorithm: boolean;
+interface Props {
+  groupId: number;
 }
 
-const devices = ref<DeviceAlgorithmItem[]>([
-  { deviceName: '五期-1#冷水主机', deviceStatus: 'off', isInAlgorithm: true },
-  { deviceName: '五期-2#冷水主机', deviceStatus: 'on', isInAlgorithm: true },
-  { deviceName: '五期-3#冷水主机', deviceStatus: 'off', isInAlgorithm: true },
-]);
+const props = defineProps<Props>();
+
+const devices = ref<AIOptimizeDeviceItem[]>([]);
 
-const deviceColumns = computed<ColumnType<DeviceAlgorithmItem>[]>(() => [
+const deviceColumns = computed<ColumnType<AIOptimizeDeviceItem>[]>(() => [
   {
     title: t('realTimeMonitor.deviceName'),
     dataIndex: 'deviceName',
@@ -27,41 +27,81 @@ const deviceColumns = computed<ColumnType<DeviceAlgorithmItem>[]>(() => [
   },
   {
     title: t('realTimeMonitor.deviceStatus'),
-    dataIndex: 'deviceStatus',
-    key: 'deviceStatus',
+    dataIndex: 'runningStatus',
+    key: 'runningStatus',
     width: 128,
   },
   {
     title: t('realTimeMonitor.isInAlgorithm'),
-    dataIndex: 'isInAlgorithm',
-    key: 'isInAlgorithm',
+    dataIndex: 'addingToAlg',
+    key: 'addingToAlg',
     width: 128,
   },
 ]);
 
-interface TemperatureRange {
-  timeSegment: RangeValue;
-  minTemp?: number;
-  maxTemp?: number;
-}
+const temperatureRanges = ref<TemperatureRange[]>([]);
 
-const temperatureRanges = ref<TemperatureRange[]>([
-  {
-    timeSegment: [dayjs('00:00', 'HH:mm'), dayjs('23:59', 'HH:mm')],
-    minTemp: 10.0,
-    maxTemp: 12.0,
-  },
-]);
+const isOneTempRange = computed(() => {
+  return temperatureRanges.value.length <= 1;
+});
 
 const addTimeRange = () => {
-  temperatureRanges.value.push({
-    timeSegment: [dayjs('00:00', 'HH:mm'), dayjs('23:59', 'HH:mm')],
-  });
+  temperatureRanges.value.push({} as TemperatureRange);
 };
 
 const deleteTimeRange = (index: number) => {
-  temperatureRanges.value.splice(index, 1);
+  if (!isOneTempRange.value) {
+    temperatureRanges.value.splice(index, 1);
+  }
 };
+
+const { handleRequest } = useRequest();
+
+onMounted(() => {
+  handleRequest(async () => {
+    const { deviceList, algorithmConfig } = await getAIOptimizeDetail(props.groupId, DeviceType.冷水主机);
+    const tempRange = JSON.parse(algorithmConfig.chilledWaterOutletTempRange || '[]') as TemperatureRangeItem[];
+    algorithmId = algorithmConfig.id;
+    devices.value = deviceList;
+
+    temperatureRanges.value = tempRange.map<TemperatureRange>((item) => ({
+      time: [dayjs(item.startTime, 'HH:mm'), dayjs(item.endTime, 'HH:mm')],
+      lower: item.lower,
+      upper: item.upper,
+    }));
+  });
+});
+
+let algorithmId: number | undefined;
+
+const submit = async () => {
+  const invalidTempRange = temperatureRanges.value.find((item) => {
+    return isEmptyVal(item.time?.[0]) || isEmptyVal(item.time?.[1]) || isEmptyVal(item.lower) || isEmptyVal(item.upper);
+  });
+
+  if (invalidTempRange) {
+    throw new Error(t('realTimeMonitor.waterTempCtrlIntervalValEmpty'));
+  }
+
+  const tempRange = temperatureRanges.value.map<TemperatureRangeItem>((item) => ({
+    startTime: item.time?.[0].format('HH:mm') as string,
+    endTime: item.time?.[1].format('HH:mm') as string,
+    lower: item.lower,
+    upper: item.upper,
+  }));
+
+  await updateAIOptimizeSetting({
+    deviceList: devices.value,
+    algorithmConfig: {
+      id: algorithmId,
+      chilledWaterOutletTempRange: JSON.stringify(tempRange),
+    },
+  });
+};
+
+defineExpose<AIOptimizeSetItemExpose>({
+  submit,
+});
 </script>
 
 <template>
@@ -74,13 +114,13 @@ const deleteTimeRange = (index: number) => {
       :pagination="false"
     >
       <template #bodyCell="{ column, record }">
-        <template v-if="column.key === 'deviceStatus'">
-          <span :class="['device-status-tag', { 'device-status-run': record.deviceStatus === 'on' }]">
-            {{ record.deviceStatus === 'on' ? $t('common.run') : $t('common.shutDown') }}
+        <template v-if="column.key === 'runningStatus'">
+          <span :class="['device-status-tag', { 'device-status-run': record.runningStatus === 1 }]">
+            {{ record.runningStatus === 1 ? $t('common.run') : $t('common.shutDown') }}
           </span>
         </template>
-        <template v-if="column.key === 'isInAlgorithm'">
-          <ASwitch v-model:checked="record.isInAlgorithm" />
+        <template v-if="column.key === 'addingToAlg'">
+          <ASwitch v-model:checked="record.addingToAlg" />
         </template>
       </template>
     </ATable>
@@ -94,7 +134,7 @@ const deleteTimeRange = (index: number) => {
     <div class="chiller-optimize-time-temp-item" v-for="(item, index) in temperatureRanges" :key="index">
       <div>
         <span class="chiller-optimize-time-temp-label">{{ $t('realTimeMonitor.timeSegment') }}</span>
-        <ATimeRangePicker v-model:value="item.timeSegment" format="HH:mm" :allow-clear="false" separator="-">
+        <ATimeRangePicker v-model:value="item.time" format="HH:mm" :allow-clear="false" separator="-">
           <template #suffixIcon>
             <SvgIcon name="clock-circle-o" :size="16" color="var(--antd-color-text-secondary)" />
           </template>
@@ -102,24 +142,24 @@ const deleteTimeRange = (index: number) => {
         <span class="chiller-optimize-time-temp-label">{{ $t('realTimeMonitor.outletTemperature') }}</span>
         <AInputNumber
           class="chiller-optimize-temp-min"
-          v-model:value="item.minTemp"
+          v-model:value="item.lower"
           :min="0"
-          :max="item.maxTemp"
+          :max="item.upper"
           :precision="1"
           :controls="false"
         />
         <span class="chiller-optimize-temp-separator">-</span>
         <AInputNumber
           class="chiller-optimize-temp-max hvac-input-number"
-          v-model:value="item.maxTemp"
-          :min="0"
-          :max="25"
+          v-model:value="item.upper"
+          :min="item.lower"
+          :max="999"
           :precision="1"
           :controls="false"
           addon-after="℃"
         />
       </div>
-      <SvgIcon name="delete" @click="deleteTimeRange(index)" />
+      <SvgIcon :class="{ 'delete-icon-disable': isOneTempRange }" name="delete" @click="deleteTimeRange(index)" />
     </div>
   </div>
 </template>
@@ -212,6 +252,11 @@ const deleteTimeRange = (index: number) => {
     font-size: 21px;
     color: var(--antd-color-primary);
     cursor: pointer;
+
+    &.delete-icon-disable {
+      cursor: not-allowed;
+      opacity: 0.6;
+    }
   }
 
   :deep(input) {

+ 49 - 12
src/views/real-time-monitor/device-optimize/CoolingTowerOptimize.vue

@@ -1,30 +1,40 @@
 <script setup lang="ts">
-import { computed, reactive } from 'vue';
+import { computed, onMounted, reactive, ref } from 'vue';
 
+import { DeviceType } from '@/views/device-work-status/device-card';
+import { useRequest } from '@/hooks/request';
 import { t } from '@/i18n';
+import { addAlgorithmConfigUpdate, getAIOptimizeDetail } from '@/api';
 
-import type { FormRules } from '@/types';
+import type { FormInstance } from 'ant-design-vue';
+import type { AIOptimizeSetItemExpose, FormRules } from '@/types';
+
+interface Props {
+  groupId: number;
+}
+
+const props = defineProps<Props>();
 
 interface CoolingTowerForm {
-  maxTemp?: number;
-  minTemp?: number;
+  coolingPipeDynamicUpper?: number;
+  coolingPipeDynamicLower?: number;
 }
 
 const coolingTowerForm = reactive<CoolingTowerForm>({
-  maxTemp: undefined,
-  minTemp: undefined,
+  coolingPipeDynamicUpper: undefined,
+  coolingPipeDynamicLower: undefined,
 });
 
 const rules = computed<FormRules<CoolingTowerForm>>(() => {
   return {
-    maxTemp: [
+    coolingPipeDynamicUpper: [
       {
         required: true,
         message: t('common.plzEnter', { name: t('realTimeMonitor.towerOutWaterTempUpperLimit') }),
         trigger: 'blur',
       },
     ],
-    minTemp: [
+    coolingPipeDynamicLower: [
       {
         required: true,
         message: t('common.plzEnter', { name: t('realTimeMonitor.towerOutWaterTempLowerLimit') }),
@@ -33,23 +43,50 @@ const rules = computed<FormRules<CoolingTowerForm>>(() => {
     ],
   };
 });
+
+const { handleRequest } = useRequest();
+
+onMounted(() => {
+  handleRequest(async () => {
+    const { algorithmConfig } = await getAIOptimizeDetail(props.groupId, DeviceType.冷却泵);
+    algorithmId = algorithmConfig.id;
+    coolingTowerForm.coolingPipeDynamicUpper = algorithmConfig.coolingPipeDynamicUpper;
+    coolingTowerForm.coolingPipeDynamicLower = algorithmConfig.coolingPipeDynamicLower;
+  });
+});
+
+let algorithmId: number | undefined;
+const formRef = ref<FormInstance>();
+
+const submit = async () => {
+  await formRef.value?.validate();
+
+  await addAlgorithmConfigUpdate({
+    id: algorithmId,
+    ...coolingTowerForm,
+  });
+};
+
+defineExpose<AIOptimizeSetItemExpose>({
+  submit,
+});
 </script>
 
 <template>
   <AForm ref="formRef" :model="coolingTowerForm" :rules="rules" layout="vertical">
-    <AFormItem :label="t('realTimeMonitor.towerOutWaterTempUpperLimit')" name="maxTemp">
+    <AFormItem :label="t('realTimeMonitor.towerOutWaterTempUpperLimit')" name="coolingPipeDynamicUpper">
       <AInputNumber
         class="hvac-input-number tower-temp-input"
-        v-model:value="coolingTowerForm.maxTemp"
+        v-model:value="coolingTowerForm.coolingPipeDynamicUpper"
         :min="0"
         :max="50"
         addon-after="℃"
       />
     </AFormItem>
-    <AFormItem :label="t('realTimeMonitor.towerOutWaterTempLowerLimit')" name="minTemp">
+    <AFormItem :label="t('realTimeMonitor.towerOutWaterTempLowerLimit')" name="coolingPipeDynamicLower">
       <AInputNumber
         class="hvac-input-number tower-temp-input"
-        v-model:value="coolingTowerForm.minTemp"
+        v-model:value="coolingTowerForm.coolingPipeDynamicLower"
         :min="0"
         :max="50"
         addon-after="℃"