Browse Source

feat(views): 添加“实时监测”页面的设备批量操作对话框

wangcong 1 tháng trước cách đây
mục cha
commit
c5a4542977

+ 12 - 0
src/i18n/locales/zh.json

@@ -99,6 +99,7 @@
     "aiCtrl": "AI智控",
     "all": "所有",
     "andCloseModal": "并关闭对话框?",
+    "automatic": "自动",
     "basicInfo": "基础信息",
     "batchExecution": "批量操作",
     "batchSetting": "批量设置",
@@ -106,6 +107,8 @@
     "cancel": "取消",
     "cannotEmpty": "不能为空",
     "certain": "确定",
+    "chilledWaterPump": "冷冻泵",
+    "chilledWaterUnit": "冷水主机",
     "clearValue": "清零",
     "clearValueComplete": "清零完成",
     "configured": "已配置",
@@ -113,6 +116,8 @@
     "confirmClose": "确定关闭{name}",
     "confirmDeletion": "是否确认删除?",
     "confirmOpen": "确定开启{name}",
+    "coolingPump": "冷却泵",
+    "coolingTower": "冷却塔",
     "custom": "自定义",
     "dataCenter": "数据中心",
     "date": "日",
@@ -134,6 +139,7 @@
     "item": "项",
     "keep": "保持",
     "keepNo": "不保持",
+    "manual": "手动",
     "maximize": "最大化",
     "month": "月",
     "needHelp": "需要帮助?",
@@ -528,6 +534,11 @@
     "autoManual": "自动/手动",
     "chilledWaterOutletSetValue": "冷冻水出水温度设定值(℃)",
     "chilledWaterOutletTemperature": "冷冻水出水温度",
+    "confirmDeviceDisable": "确定禁用设备?",
+    "confirmDeviceEnable": "确定启用设备?",
+    "confirmSwitchStartStop": "确定将{name}切换至{status}状态?",
+    "confirmSwitchToAutoTip": "调整为自动后,设备将由系统自动控制,无法手动控制",
+    "confirmSwitchToManualTip": "调整为手动后,设备将不参与系统自动控制",
     "confirmSwitchToMode": "确定切换至{mode}",
     "devParams": {
       "addMachineDelay1": "加机延时1(min)",
@@ -597,6 +608,7 @@
     "loadRate": "负载率",
     "loadRateLimitSetValue": "负载率限制设定值(%)",
     "localRemoteStatus": "本地远程状态",
+    "manualStartStop": "手动启停",
     "optimizationAlgorithm": "寻优算法",
     "otherParameters": "其他参数",
     "outletTemperature": "出水温度",

+ 338 - 0
src/views/real-time-monitor/DeviceBatchExe.vue

@@ -0,0 +1,338 @@
+<script setup lang="ts">
+import { computed, ref } from 'vue';
+import { message, Modal } from 'ant-design-vue';
+
+import SvgIcon from '@/components/SvgIcon.vue';
+import { useViewVisible } from '@/hooks/view-visible';
+import { t } from '@/i18n';
+
+import { DeviceType } from '../device-work-status/device-card';
+
+import type { SegmentedBaseOption } from 'ant-design-vue/es/segmented/src/segmented';
+import type { IconObject } from '@/types';
+
+const { visible, showView, hideView } = useViewVisible();
+const activeKey = ref('manualStartStop');
+const activeType = ref([DeviceType.冷水主机, DeviceType.冷却塔, DeviceType.冷冻泵, DeviceType.冷却泵]);
+
+const isStartStop = computed(() => {
+  return activeKey.value === 'manualStartStop';
+});
+
+interface DeviceItem {
+  id: number;
+  name: string;
+  type: DeviceType.冷水主机 | DeviceType.冷却塔 | DeviceType.冷冻泵 | DeviceType.冷却泵;
+  status: 'on' | 'off';
+  enable: boolean;
+  auto: boolean;
+}
+
+interface DeviceTypeGroupItem {
+  name: string;
+  icon: IconObject;
+  type: DeviceType.冷水主机 | DeviceType.冷却塔 | DeviceType.冷冻泵 | DeviceType.冷却泵;
+  list: DeviceItem[];
+}
+
+const deviceTypeGroups = computed<DeviceTypeGroupItem[]>(() => {
+  return [
+    {
+      name: t('common.chilledWaterUnit'),
+      icon: {
+        name: 'chiller-unit',
+        size: 21,
+      },
+      type: DeviceType.冷水主机,
+      list: [
+        { id: 1, name: '五期-1#冷水主机', type: DeviceType.冷水主机, status: 'off', enable: true, auto: true },
+        { id: 2, name: '五期-2#冷水主机', type: DeviceType.冷水主机, status: 'on', enable: false, auto: false },
+        { id: 3, name: '五期-3#冷水主机', type: DeviceType.冷水主机, status: 'off', enable: true, auto: true },
+      ],
+    },
+    {
+      name: t('common.coolingTower'),
+      icon: {
+        name: 'cooling-tower',
+        size: 23,
+      },
+      type: DeviceType.冷却塔,
+      list: [
+        { id: 4, name: '五期-1#冷却塔', type: DeviceType.冷却塔, status: 'off', enable: true, auto: true },
+        { id: 5, name: '五期-2#冷却塔', type: DeviceType.冷却塔, status: 'off', enable: true, auto: true },
+        { id: 6, name: '五期-3#冷却塔', type: DeviceType.冷却塔, status: 'off', enable: true, auto: true },
+      ],
+    },
+    {
+      name: t('common.chilledWaterPump'),
+      icon: {
+        name: 'cooling-pump',
+        size: 20,
+      },
+      type: DeviceType.冷冻泵,
+      list: [
+        { id: 7, name: '五期-1#冷冻泵', type: DeviceType.冷冻泵, status: 'off', enable: true, auto: true },
+        { id: 8, name: '五期-2#冷冻泵', type: DeviceType.冷冻泵, status: 'off', enable: true, auto: true },
+        { id: 9, name: '五期-3#冷冻泵', type: DeviceType.冷冻泵, status: 'off', enable: true, auto: true },
+      ],
+    },
+    {
+      name: t('common.coolingPump'),
+      icon: {
+        name: 'cooling-pump',
+        size: 20,
+      },
+      type: DeviceType.冷却泵,
+      list: [
+        { id: 10, name: '五期-1#冷却泵', type: DeviceType.冷却泵, status: 'off', enable: true, auto: true },
+        { id: 11, name: '五期-2#冷却泵', type: DeviceType.冷却泵, status: 'off', enable: true, auto: true },
+        { id: 12, name: '五期-3#冷却泵', type: DeviceType.冷却泵, status: 'off', enable: true, auto: true },
+      ],
+    },
+  ];
+});
+
+type StartStopValue = 'true' | 'false';
+
+interface StartStopTypeItem extends SegmentedBaseOption {
+  value: StartStopValue;
+  payload: {
+    title: string;
+    confirmTip: string;
+  };
+}
+
+const startStopTypes = computed<StartStopTypeItem[]>(() => {
+  return [
+    {
+      value: 'true',
+      payload: {
+        title: t('common.automatic'),
+        confirmTip: t('realTimeMonitor.confirmSwitchToAutoTip'),
+      },
+    },
+    {
+      value: 'false',
+      payload: {
+        title: t('common.manual'),
+        confirmTip: t('realTimeMonitor.confirmSwitchToManualTip'),
+      },
+    },
+  ];
+});
+
+const handleStartStopClick = (option: StartStopTypeItem, device: DeviceItem) => {
+  if (option.disabled || option.value === String(device.auto)) {
+    return;
+  }
+
+  Modal.confirm({
+    title: t('realTimeMonitor.confirmSwitchStartStop', {
+      name: device.name,
+      status: option.payload.title,
+    }),
+    content: option.payload.confirmTip,
+    closable: true,
+    async onOk() {
+      try {
+        device.auto = option.value === 'true';
+      } catch (err) {
+        message.error((err as Error).message);
+        console.error(err);
+      }
+    },
+  });
+};
+
+defineExpose({
+  showView,
+  hideView,
+});
+</script>
+
+<template>
+  <AModal
+    v-model:open="visible"
+    wrap-class-name="hvac-modal device-batch-modal"
+    :centered="true"
+    :width="920"
+    @ok="hideView"
+  >
+    <template #title>
+      <ATabs class="button-tabs-compact device-batch-tabs" v-model:active-key="activeKey" type="card">
+        <ATabPane key="manualStartStop" :tab="$t('realTimeMonitor.manualStartStop')" />
+        <ATabPane key="enableDisable" :tab="$t('realTimeMonitor.enableDisable')" />
+      </ATabs>
+    </template>
+    <ACollapse
+      class="device-batch-collapse"
+      v-model:active-key="activeType"
+      expand-icon-position="end"
+      :bordered="false"
+      ghost
+    >
+      <ACollapsePanel v-for="item in deviceTypeGroups" :key="item.type" :show-arrow="false">
+        <template #header>
+          <span class="device-batch-item-header">
+            <SvgIcon v-bind="item.icon" />
+            <span>{{ item.name }}</span>
+          </span>
+        </template>
+        <div class="device-batch-list-item" v-for="device in item.list" :key="device.id">
+          <span
+            v-show="isStartStop"
+            :class="[
+              'status-dot',
+              'device-batch-list-status',
+              {
+                'device-batch-status-offline': device.status === 'off',
+              },
+            ]"
+          ></span>
+          <div class="device-batch-list-name">{{ device.name }}</div>
+          <ASwitch v-show="!isStartStop" v-model:checked="device.enable" />
+          <ASegmented v-show="isStartStop" :value="String(device.auto)" :options="startStopTypes">
+            <template #label="option">
+              <span @click="handleStartStopClick(option as StartStopTypeItem, device)">{{ option.payload.title }}</span>
+            </template>
+          </ASegmented>
+        </div>
+      </ACollapsePanel>
+    </ACollapse>
+  </AModal>
+</template>
+
+<style lang="scss">
+.device-batch-modal {
+  .ant-modal-body {
+    max-height: 650px;
+    overflow-y: auto;
+  }
+}
+
+.device-batch-tabs {
+  .ant-tabs-nav {
+    margin-bottom: 0;
+  }
+}
+
+.device-batch-collapse.ant-collapse {
+  > .ant-collapse-item > {
+    .ant-collapse-header {
+      padding: 16px 24px;
+      background-color: #f5f6f7;
+    }
+
+    .ant-collapse-content > .ant-collapse-content-box {
+      padding: 0;
+    }
+  }
+
+  .ant-collapse-item + .ant-collapse-item {
+    margin-top: 24px;
+  }
+}
+</style>
+
+<style lang="scss" scoped>
+.device-batch-item-header {
+  display: flex;
+  align-items: center;
+
+  i {
+    width: 21px;
+    height: 20px;
+    margin-right: 16px;
+    color: var(--antd-color-primary);
+  }
+
+  span {
+    font-size: 14px;
+    font-weight: 600;
+    line-height: 24px;
+    color: #000;
+  }
+}
+
+.device-batch-list-item {
+  display: flex;
+  align-items: center;
+  height: 56px;
+  padding-inline: 24px;
+  border-bottom: 1px solid #e4e7ed;
+
+  .ant-switch {
+    background: #f56c6c;
+
+    &:hover:not(.ant-switch-disabled) {
+      background: #ff9797;
+    }
+
+    &.ant-switch-checked {
+      background: var(--antd-color-primary);
+
+      &:hover:not(.ant-switch-disabled) {
+        background: var(--antd-color-primary-hover);
+      }
+    }
+  }
+
+  :deep(.ant-segmented) {
+    padding: 4px;
+    color: var(--antd-color-primary);
+    background-color: var(--antd-color-primary-opacity-15);
+    border-radius: 8px;
+
+    .ant-segmented-group > div {
+      background-color: var(--antd-color-primary);
+    }
+
+    .ant-segmented-item-label {
+      display: flex;
+      align-items: center;
+      padding-inline: 0;
+
+      span {
+        padding: 5px 16px;
+        font-size: 14px;
+        font-weight: 500;
+        line-height: 22px;
+      }
+    }
+
+    .ant-segmented-item-selected {
+      color: #fff;
+      background-color: var(--antd-color-primary);
+      box-shadow: none;
+    }
+
+    .ant-segmented-item {
+      + .ant-segmented-item {
+        margin-left: 4px;
+      }
+
+      &:hover:not(.ant-segmented-item-selected, .ant-segmented-item-disabled) {
+        color: var(--antd-color-primary);
+
+        &::after {
+          background-color: var(--antd-color-primary-opacity-15);
+        }
+      }
+    }
+  }
+}
+
+.device-batch-list-status {
+  margin-right: 12px;
+}
+
+.device-batch-status-offline {
+  --status-dot-rgb: 191, 191, 191; // RGB 颜色分量 (灰色)
+}
+
+.device-batch-list-name {
+  flex: 1;
+  line-height: 24px;
+  color: var(--antd-color-text-secondary);
+}
+</style>

+ 4 - 0
src/views/real-time-monitor/RealTimeMonitor.vue

@@ -2,17 +2,21 @@
 import { useTemplateRef } from 'vue';
 
 import DeviceControl from './device-control/DeviceControl.vue';
+import DeviceBatchExe from './DeviceBatchExe.vue';
 import DeviceCtrlModal from './DeviceCtrlModal.vue';
 
 const deviceControlRef = useTemplateRef('deviceControl');
 const deviceCtrlModalRef = useTemplateRef('deviceCtrlModal');
+const deviceBatchExeRef = useTemplateRef('deviceBatchExe');
 </script>
 
 <template>
   <div class="real-time-monitor-container" @click="deviceControlRef?.closeCtrlPanel">
     <AButton @click="deviceCtrlModalRef?.showView">Dev Modal</AButton>
+    <AButton @click="deviceBatchExeRef?.showView">Dev Batch</AButton>
     <DeviceControl ref="deviceControl" />
     <DeviceCtrlModal ref="deviceCtrlModal" />
+    <DeviceBatchExe ref="deviceBatchExe" />
   </div>
 </template>