1
0

12 Commits b55685b8a2 ... b0ab460afb

Autor SHA1 Mensagem Data
  wangcong b0ab460afb perf(views): 优化设备工况页面 há 1 semana atrás
  wangcong 1d0970d64c perf(views): 优化设备工况卡片实时数据的显示 há 1 semana atrás
  wangcong 7550657a1a perf(views): 优化“设备工况”模块参数的历史数据曲线,添加数据缩放 há 1 semana atrás
  wangcong dfd46052e4 chore(i18n): 更新多语言 há 1 semana atrás
  wangcong 854f36df33 perf(views): 优化设备工况卡片频率的显示 há 1 semana atrás
  wangcong 5c13587b94 perf(components): 优化文字进度条组件样式 há 1 semana atrás
  wangshun 7cee866843 perf(views): 优化"角色管理"页面名称校验规则 há 1 semana atrás
  wangcong 5e42164dd4 perf(components): 优化设备批量操作对话框中设备的在线和离线状态的显示 há 1 semana atrás
  wangcong fab0cca8d5 feat(views): 优化设备工况卡片在线和离线状态的显示与查询 há 1 semana atrás
  wangcong 7ffa38fefc feat(views): 优化设备工况卡片提示 há 1 semana atrás
  wangcong 2d3a062e71 chore(styles): 优化"状态渐变圆点"的全局样式 há 1 semana atrás
  wangcong 1ec058e841 feat(views): 优化设备工况卡片,显示最新的时间 há 1 semana atrás

+ 1 - 0
src/components/ProgressTextBar.vue

@@ -75,6 +75,7 @@ onMounted(() => {
     .ant-progress-bg {
       display: flex;
       align-items: center;
+      background-color: var(--antd-color-primary-border-hover);
     }
 
     .ant-progress-bg::before {

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

@@ -117,6 +117,7 @@
     "confirmClose": "确定关闭{name}",
     "confirmDeletion": "是否确认删除?",
     "confirmOpen": "确定开启{name}",
+    "content": "内容",
     "coolingPump": "冷却泵",
     "coolingTower": "冷却塔",
     "custom": "自定义",
@@ -124,10 +125,12 @@
     "date": "日",
     "delete": "删除",
     "deleteConfirmation": "删除确定",
+    "device": "设备",
     "disable": "禁用",
     "editor": "编辑",
     "emptyData": "请求数据为空",
     "enable": "启用",
+    "endTime": "结束时间",
     "entire": "全部",
     "exitEdit": "退出编辑",
     "export": "导出",
@@ -139,6 +142,7 @@
     "group": "组",
     "hour": "时",
     "inoperableReason": "不可操作原因",
+    "inputContent": "输入内容",
     "item": "项",
     "keep": "保持",
     "keepNo": "不保持",
@@ -159,6 +163,9 @@
     "online": "在线",
     "open": "开启",
     "operation": "操作",
+    "operationContent": "操作内容",
+    "operationResult": "操作结果",
+    "operator": "操作人员",
     "opsCenter": "运维中心",
     "pageTotal": "共 {total} 条",
     "pleaseEnter": "请输入",
@@ -178,6 +185,7 @@
     "reset": "重置",
     "resetFlag": "复位",
     "resetFlagComplete": "复位完成",
+    "result": "结果",
     "return": "返回",
     "returnFirstUse": "返回到首次使用页?",
     "revise": "修改",
@@ -197,11 +205,13 @@
     "skip": "跳过",
     "skipConfirm": "跳过确认",
     "skipStepConfirm": "是否跳过{name},",
+    "startTime": "开始时间",
     "status": "状态",
     "submit": "提交",
     "success": "成功",
     "templateImport": "模板导入",
     "tenThousand": "万",
+    "time": "时间",
     "tip": "提示",
     "to": "至",
     "total": "共",
@@ -520,7 +530,14 @@
   "keywordLibrary": {},
   "largeScreen": {},
   "logCenter": {
+    "deviceParameterChange": "设备参数变化",
     "operationLogs": "操作日志",
+    "plzEnterContent": "请输入内容",
+    "plzSelectDevice": "请选择设备",
+    "plzSelectDeviceGroup": "请选择设备组",
+    "selectDevice": "选择设备",
+    "selectDeviceGroup": "选择设备组",
+    "selectTimeRange": "选择时间段",
     "smartControlLogs": "智控日志"
   },
   "navigation": {

+ 5 - 1
src/styles/global.scss

@@ -288,7 +288,7 @@
 
 .status-dot {
   // 定义控制变量 (可外部覆盖)
-  --status-dot-rgb: 103, 194, 58; // RGB 颜色分量 (绿色)
+  --status-dot-rgb: 191, 191, 191; // RGB 颜色分量 (灰色)
 
   position: relative;
   display: inline-block;
@@ -323,6 +323,10 @@
   }
 }
 
+.status-dot-green {
+  --status-dot-rgb: 103, 194, 58; // RGB 颜色分量 (绿色)
+}
+
 .center-loading {
   position: absolute;
   inset: 0;

+ 1 - 0
src/types/index.ts

@@ -893,6 +893,7 @@ export interface DevicesList {
   deviceName?: string;
   runningStatus?: number;
   runningStatusList?: DeviceRunningStatus[] | null;
+  status?: number | null;
   errorStatus?: number;
 }
 

+ 31 - 2
src/views/device-work-status/DevWorkParamData.vue

@@ -6,6 +6,7 @@ import { LineChart } from 'echarts/charts';
 import {
   AxisPointerComponent,
   DatasetComponent,
+  DataZoomComponent,
   GridComponent,
   LegendComponent,
   MarkLineComponent,
@@ -26,6 +27,7 @@ import type { Dayjs } from 'dayjs';
 import type { LineSeriesOption } from 'echarts/charts';
 import type {
   DatasetComponentOption,
+  DataZoomComponentOption,
   GridComponentOption,
   LegendComponentOption,
   TooltipComponentOption,
@@ -42,10 +44,16 @@ use([
   DatasetComponent,
   GridComponent,
   LineChart,
+  DataZoomComponent,
 ]);
 
 type EChartsOption = ComposeOption<
-  LegendComponentOption | TooltipComponentOption | DatasetComponentOption | GridComponentOption | LineSeriesOption
+  | LegendComponentOption
+  | TooltipComponentOption
+  | DatasetComponentOption
+  | GridComponentOption
+  | LineSeriesOption
+  | DataZoomComponentOption
 >;
 
 interface Props {
@@ -188,10 +196,31 @@ const option = computed<EChartsOption>(() => {
     grid: {
       top: 10,
       right: 30,
-      bottom: 20,
+      bottom: 90,
       left: 30,
       containLabel: true,
     },
+    dataZoom: [
+      {
+        type: 'inside',
+        start: 0,
+        end: 100,
+        bottom: 35,
+        handleStyle: {
+          label: { show: false },
+        },
+        showDetail: false,
+      },
+      {
+        start: 0,
+        end: 100,
+        bottom: 35,
+        handleStyle: {
+          label: { show: false },
+        },
+        showDetail: false,
+      },
+    ],
   };
 });
 

+ 26 - 14
src/views/device-work-status/DeviceWorkStatus.vue

@@ -6,8 +6,8 @@ import { useInfiniteScroll } from '@vueuse/core';
 import SvgIcon from '@/components/SvgIcon.vue';
 import { useRequest } from '@/hooks/request';
 import { getDevWorkRealTimeData, getDevWorkTypeCount, queryDevicesList } from '@/api';
-import { getFixedNum } from '@/utils';
-import { DeviceRunningStatus, DeviceStatusQuery } from '@/constants';
+import { getFixedNum, timeSorter } from '@/utils';
+import { DeviceStatusQuery } from '@/constants';
 import { DevParamChillerUnit, DevParamCtrlCabinet } from '@/constants/device-params';
 
 import { deviceCardData, DeviceType } from './device-card';
@@ -124,19 +124,19 @@ useInfiniteScroll(
 
 const getDeviceList = () => {
   handleRequest(async () => {
-    let runningStatusList = null;
+    let status = null;
 
     if (activeDeviceStatus.value === DeviceStatusQuery.Offline) {
-      runningStatusList = [DeviceRunningStatus.Offline];
+      status = 0;
     } else if (activeDeviceStatus.value === DeviceStatusQuery.Online) {
-      runningStatusList = [DeviceRunningStatus.Stop, DeviceRunningStatus.Run];
+      status = 1;
     }
 
     const { records, total } = await queryDevicesList({
       ...pageParams.value,
       deviceType: activeDeviceType.value,
       groupId: props.deviceGroupId,
-      runningStatusList,
+      status,
     });
 
     const deviceIds = records.map((item) => item.id);
@@ -152,9 +152,19 @@ const getDeviceList = () => {
         deviceRealTimeData.value[deviceId] = {};
       }
 
-      deviceParamMapList.forEach((paramItem) => {
-        Object.assign(deviceRealTimeData.value[item.deviceId], paramItem);
-      });
+      const times = deviceParamMapList
+        .filter((item) => item.time)
+        .map((item) => item.time)
+        .sort(timeSorter);
+      const mostRecentTime = times[times.length - 1];
+
+      if (mostRecentTime) {
+        deviceParamMapList.forEach((paramItem) => {
+          if (paramItem.time === mostRecentTime) {
+            Object.assign(deviceRealTimeData.value[item.deviceId], paramItem);
+          }
+        });
+      }
 
       if (isDeviceChillerUnit) {
         Object.assign(deviceRealTimeData.value[item.deviceId], chillerUnitExtraParams);
@@ -238,6 +248,7 @@ const handleDevCardClick = (devId: number, e: Event) => {
       </ATabs>
     </div>
     <div ref="deviceListEl" class="device-card-list">
+      <AEmpty class="device-card-empty" v-show="!deviceList.length" />
       <ARow :gutter="[22, 24]">
         <ACol v-for="item in deviceList" :key="item.id" :xs="24" :sm="24" :md="24" :lg="24" :xl="12" :xxl="8">
           <div class="device-card-container" @click="handleDevCardClick(item.id, $event)">
@@ -246,7 +257,7 @@ const handleDevCardClick = (devId: number, e: Event) => {
                 :class="[
                   'status-dot',
                   {
-                    'device-status-offline': item.runningStatus === DeviceRunningStatus.Offline,
+                    'status-dot-green': item.status === 1,
                   },
                 ]"
               ></span>
@@ -284,6 +295,7 @@ const handleDevCardClick = (devId: number, e: Event) => {
           </div>
         </ACol>
       </ARow>
+      <ASpin v-if="isLoading" class="center-loading" :spinning="true" />
     </div>
     <DeviceWorkParams ref="deviceWorkParams" :dev-id="currentDevId" @view-history-data="viewHistoryData" />
     <DevWorkParamData ref="devWorkParamData" :dev-id="currentDevId" :param-codes="currentDevParamCodes" />
@@ -332,6 +344,10 @@ const handleDevCardClick = (devId: number, e: Event) => {
   overflow: hidden auto;
 }
 
+.device-card-empty {
+  margin-top: 200px;
+}
+
 .device-card-container {
   position: relative;
   height: 328px;
@@ -346,10 +362,6 @@ const handleDevCardClick = (devId: number, e: Event) => {
   margin-bottom: 24px;
 }
 
-.device-status-offline {
-  --status-dot-rgb: 191, 191, 191; // RGB 颜色分量 (灰色)
-}
-
 .device-card-header-title {
   max-width: 150px;
   margin-right: 16px;

+ 33 - 7
src/views/device-work-status/device-card/CoolingPump.vue

@@ -2,7 +2,8 @@
 import { computed } from 'vue';
 
 import ProgressTextBar from '@/components/ProgressTextBar.vue';
-import { calcPercentage, getFixedNum } from '@/utils';
+import { t } from '@/i18n';
+import { calcPercentage, getFixedNum, isNotEmptyVal } from '@/utils';
 import { DevParamCoolingPump } from '@/constants/device-params';
 
 import type { DevWorkCardProps } from '@/types';
@@ -15,11 +16,29 @@ const activePowerPercent = computed(() => {
   return calcPercentage(currentPower, maxPower);
 });
 
+const activePowerTip = computed(() => {
+  const activePower = props.realTimeData?.[DevParamCoolingPump.有功功率];
+  const powerRating = props.deviceDetail.powerRating;
+
+  const isActivePowerNotEmpty = isNotEmptyVal(activePower);
+  const isPowerRatingNotEmpty = isNotEmptyVal(powerRating);
+  const percent = isActivePowerNotEmpty && isPowerRatingNotEmpty ? activePowerPercent.value : '-';
+
+  return `${t('deviceWorkStatus.chillerUnit.activePowerPercentage')}: ${getFixedNum(percent, 1)}%
+${t('deviceWorkStatus.chillerUnit.activePower')}: ${getFixedNum(activePower, 1)}kW
+${t('deviceList.ratedPower')}: ${getFixedNum(powerRating, 1)}kW`;
+});
+
 const frequencyFbPercent = computed(() => {
   const currentFrequency = props.realTimeData?.[DevParamCoolingPump.频率反馈] as number;
   const maxFrequency = 50;
   return calcPercentage(currentFrequency, maxFrequency);
 });
+
+const showFrequency = computed(() => {
+  const frequencyConversion = props.deviceDetail.frequencyConversion as string | undefined;
+  return frequencyConversion?.includes('变频') || frequencyConversion?.includes('inverter');
+});
 </script>
 
 <template>
@@ -35,13 +54,16 @@ const frequencyFbPercent = computed(() => {
         </div>
         <div>
           <div class="device-card-label">{{ $t('deviceWorkStatus.coolingTower.activePower') }} (kW)</div>
-          <ProgressTextBar
-            :text="getFixedNum(realTimeData?.[DevParamCoolingPump.有功功率], 1)"
-            :percent="activePowerPercent"
-            :data-param-code="DevParamCoolingPump.有功功率"
-          />
+          <ATooltip overlay-class-name="hvac-tooltip">
+            <template #title>{{ activePowerTip }}</template>
+            <ProgressTextBar
+              :text="getFixedNum(realTimeData?.[DevParamCoolingPump.有功功率], 1)"
+              :percent="activePowerPercent"
+              :data-param-code="DevParamCoolingPump.有功功率"
+            />
+          </ATooltip>
         </div>
-        <div>
+        <div v-if="showFrequency">
           <div class="device-card-label">{{ $t('deviceWorkStatus.coolingTower.frequencyFb') }} (Hz)</div>
           <ProgressTextBar
             :text="getFixedNum(realTimeData?.[DevParamCoolingPump.频率反馈], 0)"
@@ -49,6 +71,10 @@ const frequencyFbPercent = computed(() => {
             :data-param-code="DevParamCoolingPump.频率反馈"
           />
         </div>
+        <div v-else>
+          <div class="device-card-label"></div>
+          <div class="device-card-value device-card-no-history"></div>
+        </div>
       </div>
     </div>
     <div class="cooling-pump-bottom">

+ 33 - 7
src/views/device-work-status/device-card/CoolingTower.vue

@@ -2,7 +2,8 @@
 import { computed } from 'vue';
 
 import ProgressTextBar from '@/components/ProgressTextBar.vue';
-import { calcPercentage, getFixedNum } from '@/utils';
+import { t } from '@/i18n';
+import { calcPercentage, getFixedNum, isNotEmptyVal } from '@/utils';
 import { DevParamCoolingTower } from '@/constants/device-params';
 
 import type { DevWorkCardProps } from '@/types';
@@ -15,11 +16,29 @@ const activePowerPercent = computed(() => {
   return calcPercentage(currentPower, maxPower);
 });
 
+const activePowerTip = computed(() => {
+  const activePower = props.realTimeData?.[DevParamCoolingTower.有功功率];
+  const powerRating = props.deviceDetail.powerRating;
+
+  const isActivePowerNotEmpty = isNotEmptyVal(activePower);
+  const isPowerRatingNotEmpty = isNotEmptyVal(powerRating);
+  const percent = isActivePowerNotEmpty && isPowerRatingNotEmpty ? activePowerPercent.value : '-';
+
+  return `${t('deviceWorkStatus.chillerUnit.activePowerPercentage')}: ${getFixedNum(percent, 1)}%
+${t('deviceWorkStatus.chillerUnit.activePower')}: ${getFixedNum(activePower, 1)}kW
+${t('deviceList.ratedPower')}: ${getFixedNum(powerRating, 1)}kW`;
+});
+
 const frequencyFbPercent = computed(() => {
   const currentFrequency = props.realTimeData?.[DevParamCoolingTower.频率反馈] as number;
   const maxFrequency = 50;
   return calcPercentage(currentFrequency, maxFrequency);
 });
+
+const showFrequency = computed(() => {
+  const frequencyConversion = props.deviceDetail.frequencyConversion as string | undefined;
+  return frequencyConversion?.includes('变频') || frequencyConversion?.includes('inverter');
+});
 </script>
 
 <template>
@@ -35,13 +54,16 @@ const frequencyFbPercent = computed(() => {
         </div>
         <div>
           <div class="device-card-label">{{ $t('deviceWorkStatus.coolingTower.activePower') }} (kW)</div>
-          <ProgressTextBar
-            :text="getFixedNum(realTimeData?.[DevParamCoolingTower.有功功率], 1)"
-            :percent="activePowerPercent"
-            :data-param-code="DevParamCoolingTower.有功功率"
-          />
+          <ATooltip overlay-class-name="hvac-tooltip">
+            <template #title>{{ activePowerTip }}</template>
+            <ProgressTextBar
+              :text="getFixedNum(realTimeData?.[DevParamCoolingTower.有功功率], 1)"
+              :percent="activePowerPercent"
+              :data-param-code="DevParamCoolingTower.有功功率"
+            />
+          </ATooltip>
         </div>
-        <div>
+        <div v-if="showFrequency">
           <div class="device-card-label">{{ $t('deviceWorkStatus.coolingTower.frequencyFb') }} (Hz)</div>
           <ProgressTextBar
             :text="getFixedNum(realTimeData?.[DevParamCoolingTower.频率反馈], 0)"
@@ -49,6 +71,10 @@ const frequencyFbPercent = computed(() => {
             :data-param-code="DevParamCoolingTower.频率反馈"
           />
         </div>
+        <div v-else>
+          <div class="device-card-label"></div>
+          <div class="device-card-value device-card-no-history"></div>
+        </div>
       </div>
     </div>
     <div class="cooling-tower-bottom">

+ 1 - 5
src/views/real-time-monitor/DeviceBatchExe.vue

@@ -234,7 +234,7 @@ defineExpose({
               'status-dot',
               'device-batch-list-status',
               {
-                'device-batch-status-offline': device.runningStatus === 0,
+                'status-dot-green': device.runningStatus === 1,
               },
             ]"
           ></span>
@@ -389,10 +389,6 @@ defineExpose({
   margin-right: 12px;
 }
 
-.device-batch-status-offline {
-  --status-dot-rgb: 191, 191, 191; // RGB 颜色分量 (灰色)
-}
-
 .device-batch-list-name {
   line-height: 24px;
   color: var(--antd-color-text-secondary);

+ 12 - 8
src/views/role-manage/RoleManage.vue

@@ -48,7 +48,7 @@ const pagePermissionsTree = ref<DataNode[]>([
   },
 ]);
 const addCharacterName = async () => {
-  if (characterList.value.some((item) => item.name === '')) {
+  if (characterList.value.some((item) => !isValidString(item.name))) {
     return;
   }
   characterList.value.push({
@@ -61,14 +61,14 @@ const addCharacterName = async () => {
   inputRef.value[0].focus();
 };
 const clickCharacter = (id: number) => {
-  if (characterList.value.some((item) => item.name === '')) {
+  if (characterList.value.some((item) => !isValidString(item.name))) {
     return;
   }
 
   characterListId.value = id;
 };
 const addEditor = async (index: number) => {
-  if (characterList.value.some((item) => item.name === '')) {
+  if (characterList.value.some((item) => !isValidString(item.name))) {
     return;
   }
   characterList.value[index].show = true;
@@ -79,6 +79,9 @@ const addDelete = (id: number | undefined) => {
   if (!id) {
     return message.warning(t('deviceList.pleaseSelectItemDelete'));
   }
+  if (characterList.value.some((item) => !isValidString(item.name))) {
+    return;
+  }
   characterId.value = id;
   modalComponentRef.value?.showView();
 };
@@ -100,16 +103,17 @@ const editorCharacterBlur = (index: number, name: string) => {
   }
 };
 /**
- * 检查字符串是否包含任何空白字符(包括空格、制表符、换行等)
+ * 校验字符串是否非空且不包含任何空白字符
  * @param str 待检测的字符串
- * @returns 是否包含空白字符
+ * @returns 是否有效(非空且无空白)
  */
-const hasWhitespace = (str: string): boolean => {
-  return /\s/.test(str);
+const isValidString = (str: string): boolean => {
+  return /^[\S]+$/.test(str);
+  // ^ 开头 | \S 非空白字符 | + 至少一个 | $ 结尾
 };
 const editorCharacter = (index: number, name: string) => {
   if (name) {
-    if (!hasWhitespace(name)) {
+    if (isValidString(name)) {
       enterShow.value = true;
       characterList.value[index].show = false;
       const id = characterList.value[index].id;

+ 0 - 1
src/views/user-manage/UserManage.vue

@@ -148,7 +148,6 @@ const addQuery = () => {
     accountPageParam.value.startTenancy = '';
     accountPageParam.value.endTenancy = '';
   }
-  console.log(searchContent.value);
   accountPageParam.value.mobile = searchContent.value;
   accountPageParam.value.userName = searchContent.value;
   accountPageParam.value.pageIndex = 1;