فهرست منبع

feat(views): 编写“设备工况”模块的设备卡片与参数组件

wangcong 2 ماه پیش
والد
کامیت
79fae9f2ab

BIN
src/assets/img/device-chiller-unit.png


+ 27 - 0
src/constants/index.ts

@@ -67,3 +67,30 @@ export const enum HumitureType {
    */
   Outdoor,
 }
+
+/**
+ * 设备状态查询
+ */
+export const enum DeviceStatusQuery {
+  All,
+  Online,
+  Offline,
+}
+
+/**
+ * 设备运行状态
+ */
+export const enum DeviceRunningStatus {
+  /**
+   * 离线
+   */
+  Offline,
+  /**
+   * 停机
+   */
+  Stop,
+  /**
+   * 运行
+   */
+  Run,
+}

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

@@ -13,6 +13,7 @@
     "delete": "删除",
     "deleteConfirmation": "删除确定",
     "emptyData": "请求数据为空",
+    "entire": "全部",
     "exportToLocal": "导出到本地",
     "failure": "失败",
     "finishSetup": "完成配置",
@@ -127,7 +128,17 @@
     "withinGroupRanking": "组内排序"
   },
   "deviceList": {},
-  "deviceWorkStatus": {},
+  "deviceWorkStatus": {
+    "chillerUnit": {
+      "activePower": "有功功率",
+      "coolingCapacity": "制冷量",
+      "loadRate": "负载率",
+      "monthSPowerConsumption": "本月耗电量",
+      "todayPowerConsumption": "今日耗电量",
+      "waterTempOutSet": "冷冻水出水温度设定值反馈"
+    },
+    "viewData": "查看数据"
+  },
   "envMonitor": {},
   "firstUsage": {
     "chooseItemToBeginSetup": "选择一项开始配置",

+ 178 - 0
src/views/device-work-status/DeviceWorkParams.vue

@@ -0,0 +1,178 @@
+<script setup lang="ts">
+import { computed, ref, watch } from 'vue';
+
+import SvgIcon from '@/components/SvgIcon.vue';
+import { useRequest } from '@/hooks/request';
+import { useViewVisible } from '@/hooks/view-visible';
+import { getDeviceParams } from '@/api';
+
+import type { DeviceParamGroup, DeviceParamItem, OptionItem } from '@/types';
+
+const { visible, showView, hideView } = useViewVisible();
+const { isLoading, handleRequest } = useRequest();
+const allParamGroups = ref<DeviceParamGroup[]>([]);
+const activeGroup = ref(-1);
+
+const groupList = computed<OptionItem<number>[]>(() => {
+  return allParamGroups.value.map((item) => ({
+    value: item.id,
+    label: item.deviceParamGroupName,
+  }));
+});
+
+const paramList = computed<DeviceParamItem[]>(() => {
+  const params = allParamGroups.value.find((item) => item.id === activeGroup.value);
+
+  if (params) {
+    return params.valueVos;
+  }
+
+  return [];
+});
+
+watch(visible, () => {
+  if (visible.value) {
+    getAllParamGroups();
+  }
+});
+
+const getAllParamGroups = () => {
+  handleRequest(async () => {
+    const data = await getDeviceParams(51, false);
+    allParamGroups.value = data;
+
+    if (data.length) {
+      activeGroup.value = data[0].id;
+    }
+  });
+};
+
+const viewData = () => {
+  return;
+};
+
+const handleClose = () => {
+  allParamGroups.value = [];
+  activeGroup.value = -1;
+};
+
+defineExpose({
+  showView,
+  hideView,
+});
+</script>
+
+<template>
+  <AModal
+    v-model:open="visible"
+    wrap-class-name="dev-work-params-modal"
+    :title="$t('createDevice.parameters')"
+    :width="920"
+    centered
+    :footer="null"
+    :after-close="handleClose"
+  >
+    <ASpin class="center-loading" :spinning="isLoading" />
+    <ATabs class="hide-tabs-border" v-model:active-key="activeGroup">
+      <ATabPane v-for="item in groupList" :key="item.value" :tab="item.label" />
+    </ATabs>
+    <div class="param-list">
+      <ARow :gutter="[16, 16]">
+        <ACol v-for="item in paramList" :key="item.deviceParamCode" :span="8">
+          <div class="param-container">
+            <div class="param-header">
+              <div class="ellipsis-text param-name">{{ item.paramName }}</div>
+              <div class="param-view" @click="viewData">{{ $t('deviceWorkStatus.viewData') }}</div>
+              <SvgIcon name="right" :size="12" color="rgba(0, 0, 0, 0.25)" />
+            </div>
+            <div class="param-body">
+              <div class="param-value">{{ item.value }}{{ item.unit }}</div>
+              <div class="param-time">{{ item.time }}</div>
+            </div>
+          </div>
+        </ACol>
+      </ARow>
+    </div>
+  </AModal>
+</template>
+
+<style lang="scss">
+.dev-work-params-modal {
+  .ant-modal,
+  .ant-modal > div,
+  .ant-modal-content {
+    height: 640px;
+  }
+
+  .ant-modal-content {
+    display: flex;
+    flex-direction: column;
+    overflow: hidden;
+  }
+
+  .ant-modal-body {
+    display: flex;
+    flex-direction: column;
+    height: calc(100% - 32px);
+  }
+}
+</style>
+
+<style lang="scss" scoped>
+.param-list {
+  height: 100%;
+  overflow: hidden auto;
+}
+
+.param-container {
+  width: 280px;
+  height: 156px;
+  background: #fff;
+  border: 1px solid #e4e7ed;
+  border-radius: 8px;
+}
+
+.param-header {
+  display: flex;
+  align-items: center;
+  height: 50px;
+  padding-right: 12px;
+  padding-left: 16px;
+  border-bottom: 1px solid #e4e7ed;
+}
+
+.param-name {
+  flex: 1;
+  margin-right: 16px;
+  font-size: 14px;
+  line-height: 22px;
+  color: var(--antd-color-text);
+}
+
+.param-view {
+  margin-right: 4px;
+  font-size: 14px;
+  font-weight: 500;
+  line-height: 24px;
+  color: var(--antd-color-primary);
+  cursor: pointer;
+}
+
+.param-body {
+  padding: 24px 16px;
+}
+
+.param-value {
+  margin-bottom: 8px;
+  font-size: 18px;
+  font-weight: 600;
+  line-height: 26px;
+  color: var(--antd-color-text);
+}
+
+.param-time {
+  font-size: 14px;
+  line-height: 24px;
+  color: var(--antd-color-text-tertiary);
+}
+</style>

+ 150 - 8
src/views/device-work-status/DeviceWorkStatus.vue

@@ -1,20 +1,30 @@
 <script setup lang="ts">
-import { onMounted, ref } from 'vue';
+import { onMounted, ref, useTemplateRef } from 'vue';
 
+import SvgIcon from '@/components/SvgIcon.vue';
 import { useRequest } from '@/hooks/request';
 import { getDevWorkRealTimeData, getDevWorkTypeCount, queryDevicesList } from '@/api';
+import { DeviceRunningStatus, DeviceStatusQuery } from '@/constants';
 
 import { deviceCardData, DeviceType } from './device-card';
+import DeviceWorkParams from './DeviceWorkParams.vue';
 
 import type { DevicesListItem, DeviceTypeCount, PageParams } from '@/types';
 
 const { handleRequest } = useRequest();
 const deviceTypes = ref<DeviceTypeCount[]>([]);
 const activeDeviceType = ref<DeviceType>(DeviceType.空);
+const activeDeviceStatus = ref<DeviceStatusQuery>(DeviceStatusQuery.All);
 
 onMounted(() => {
   handleRequest(async () => {
-    deviceTypes.value = await getDevWorkTypeCount(7);
+    deviceTypes.value = await getDevWorkTypeCount(7, [
+      DeviceType.冷水主机,
+      DeviceType.控制柜,
+      DeviceType.冷却塔,
+      DeviceType.冷却泵,
+      DeviceType.冷冻泵,
+    ]);
 
     if (deviceTypes.value.length) {
       activeDeviceType.value = deviceTypes.value[0].deviceType;
@@ -27,7 +37,7 @@ const deviceList = ref<DevicesListItem[]>([]);
 const deviceTotal = ref(0);
 const pageParams = ref<PageParams>({
   pageIndex: 1,
-  pageSize: 10,
+  pageSize: 100,
   pageSorts: [
     {
       column: 'id',
@@ -38,9 +48,18 @@ const pageParams = ref<PageParams>({
 
 const getDeviceList = () => {
   handleRequest(async () => {
+    let runningStatusList = null;
+
+    if (activeDeviceStatus.value === DeviceStatusQuery.Offline) {
+      runningStatusList = [DeviceRunningStatus.Offline];
+    } else if (activeDeviceStatus.value === DeviceStatusQuery.Online) {
+      runningStatusList = [DeviceRunningStatus.Stop, DeviceRunningStatus.Run];
+    }
+
     const { records, total } = await queryDevicesList({
       ...pageParams.value,
       deviceType: activeDeviceType.value,
+      runningStatusList,
     });
 
     const deviceIds = records.map((item) => item.id);
@@ -59,27 +78,87 @@ const handleTabClick = () => {
     getDeviceList();
   }, 0);
 };
+
+const deviceWorkParamsRef = useTemplateRef('deviceWorkParams');
+
+const viewDevParam = () => {
+  deviceWorkParamsRef.value?.showView();
+};
 </script>
 
 <template>
   <div>
-    <ATabs class="button-tabs-card" v-model:active-key="activeDeviceType" type="card" @tab-click="handleTabClick">
-      <ATabPane v-for="item in deviceTypes" :key="item.deviceType" :tab="`${item.deviceTypeName}(${item.count})`" />
-    </ATabs>
+    <div class="device-work-header">
+      <ATabs
+        class="button-tabs-card device-type-tab"
+        v-model:active-key="activeDeviceType"
+        type="card"
+        @tab-click="handleTabClick"
+      >
+        <ATabPane v-for="item in deviceTypes" :key="item.deviceType" :tab="`${item.deviceTypeName}(${item.count})`" />
+      </ATabs>
+      <ATabs class="device-status-tab" v-model:active-key="activeDeviceStatus" @tab-click="handleTabClick">
+        <ATabPane :key="DeviceStatusQuery.All" :tab="$t('common.entire')" />
+        <ATabPane :key="DeviceStatusQuery.Online" :tab="$t('common.online')" />
+        <ATabPane :key="DeviceStatusQuery.Offline" :tab="$t('common.offline')" />
+      </ATabs>
+    </div>
     <ARow :gutter="[22, 24]">
       <ACol v-for="item in deviceList" :key="item.id" :xs="24" :sm="24" :md="24" :lg="12" :xl="12" :xxl="8">
         <div class="device-card-container">
           <div class="device-card-header">
-            <div class="device-card-header-title">{{ item.deviceName }}</div>
+            <span
+              :class="[
+                'status-dot',
+                {
+                  'device-status-offline': item.runningStatus === DeviceRunningStatus.Offline,
+                },
+              ]"
+            ></span>
+            <div class="ellipsis-text device-card-header-title">{{ item.deviceName }}</div>
+            <div class="device-cop-value">5.17</div>
+            <div class="device-cop-level">中</div>
+            <span class="device-card-header-time">2025-3-21 09:40</span>
+            <SvgIcon class="device-card-header-button" name="adjustment" @click="viewDevParam" />
           </div>
           <component :is="deviceCardData[activeDeviceType]?.component" />
         </div>
       </ACol>
     </ARow>
+    <DeviceWorkParams ref="deviceWorkParams" />
   </div>
 </template>
 
 <style lang="scss" scoped>
+.device-work-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+
+.device-type-tab {
+  width: calc(100% - 200px);
+}
+
+:deep(.device-status-tab) {
+  font-weight: 500;
+  line-height: 22px;
+
+  &.ant-tabs-top > .ant-tabs-nav {
+    .ant-tabs-tab {
+      padding: 5px 0;
+
+      + .ant-tabs-tab {
+        margin-left: 24px;
+      }
+    }
+
+    &::before {
+      border: none;
+    }
+  }
+}
+
 .device-card-container {
   height: 328px;
   padding: 16px 24px;
@@ -90,14 +169,77 @@ const handleTabClick = () => {
 .device-card-header {
   display: flex;
   align-items: center;
-  justify-content: space-between;
   margin-bottom: 24px;
 }
 
+.device-status-offline {
+  --status-dot-rgb: 191, 191, 191; // RGB 颜色分量 (灰色)
+}
+
 .device-card-header-title {
+  max-width: 150px;
+  margin-right: 16px;
+  margin-left: 8px;
   font-size: 16px;
   font-weight: 600;
   line-height: 28px;
   color: var(--antd-color-text);
 }
+
+.device-cop-value {
+  font-family: DINAlternate;
+  font-size: 24px;
+  font-weight: bold;
+  line-height: 32px;
+  color: #e6a23c;
+}
+
+.device-cop-level {
+  width: 16px;
+  height: 16px;
+  margin-left: 8px;
+  font-size: 12px;
+  font-weight: 600;
+  line-height: 16px;
+  color: #e6a23c;
+  text-align: center;
+  background: rgb(234 178 94 / 40%);
+  border-radius: 2px;
+}
+
+.device-card-header-time {
+  flex: 1;
+  font-size: 12px;
+  font-weight: 500;
+  line-height: 20px;
+  color: #999;
+  text-align: right;
+}
+
+.device-card-header-button {
+  padding: 4px;
+  margin-left: 16px;
+  font-size: 16px;
+  color: var(--antd-color-primary);
+  cursor: pointer;
+  background: var(--antd-color-primary-opacity-15);
+  border-radius: 4px;
+}
+
+:deep(.device-card-label) {
+  height: 20px;
+  font-size: 12px;
+  font-weight: 500;
+  line-height: 20px;
+  color: #999;
+}
+
+:deep(.device-card-value) {
+  height: 24px;
+  font-family: DINAlternate;
+  font-size: 16px;
+  font-weight: bold;
+  line-height: 24px;
+  color: #333;
+}
 </style>

+ 63 - 3
src/views/device-work-status/device-card/ChillerUnit.vue

@@ -1,9 +1,69 @@
 <script setup lang="ts">
-import { DevParamChillerUnit } from '@/constants/device-params';
+// import { DevParamChillerUnit } from '@/constants/device-params';
 </script>
 
 <template>
-  <div>冷水主机设备参数 - {{ DevParamChillerUnit.今日耗电量 }}</div>
+  <div>
+    <div class="chiller-unit-top">
+      <div class="chiller-unit-left">
+        <div>
+          <div class="device-card-label">{{ $t('deviceWorkStatus.chillerUnit.coolingCapacity') }}(kW)</div>
+          <div>-</div>
+        </div>
+        <div>
+          <div class="device-card-label">{{ $t('deviceWorkStatus.chillerUnit.activePower') }} kW</div>
+          <div>-</div>
+        </div>
+        <div>
+          <div class="device-card-label">{{ $t('deviceWorkStatus.chillerUnit.loadRate') }}</div>
+          <div>-</div>
+        </div>
+      </div>
+      <div class="chiller-unit-img">
+        <img src="@/assets/img/device-chiller-unit.png" />
+      </div>
+    </div>
+    <div class="chiller-unit-bottom">
+      <div>
+        <div class="device-card-label">{{ $t('deviceWorkStatus.chillerUnit.waterTempOutSet') }}</div>
+        <div class="device-card-value">-</div>
+      </div>
+      <div>
+        <div class="device-card-label">{{ $t('deviceWorkStatus.chillerUnit.todayPowerConsumption') }} kWh</div>
+        <div class="device-card-value">-</div>
+      </div>
+      <div>
+        <div class="device-card-label">{{ $t('deviceWorkStatus.chillerUnit.monthSPowerConsumption') }} kWh</div>
+        <div class="device-card-value">-</div>
+      </div>
+    </div>
+  </div>
 </template>
 
-<!-- <style lang="scss" scoped></style> -->
+<style lang="scss" scoped>
+.chiller-unit-top {
+  display: flex;
+  justify-content: space-between;
+  margin-bottom: 18px;
+}
+
+.chiller-unit-left {
+  width: 170px;
+  margin-right: 28px;
+
+  > div + div {
+    margin-top: 22px;
+  }
+}
+
+.chiller-unit-img {
+  position: relative;
+  width: 316px;
+  height: 178px;
+}
+
+.chiller-unit-bottom {
+  display: flex;
+  justify-content: space-between;
+}
+</style>