Преглед изворни кода

feat(stat): 新增瑞恒看板异常警示

- 独立拆分瑞恒异常警示组件
- 接入瑞恒非生产时效统计接口
- 支持按时间范围切换异常数据
- 点击异常项跳转日报汇总的非生产时效页签
- 恢复锐德异常警示组件的独立逻辑
Zimo пре 1 месец
родитељ
комит
31d71a72d0

+ 2 - 14
src/views/pms/stat/rdkb/rd-exception-prompt.vue

@@ -6,16 +6,6 @@ import { IotStatApi } from '@/api/pms/stat'
 dayjs.extend(quarterOfYear)
 
 type TimeRange = 'day' | 'month' | 'quarter' | 'year'
-type Company = 'rd' | 'rh'
-
-const props = withDefaults(
-  defineProps<{
-    company?: Company
-  }>(),
-  {
-    company: 'rd'
-  }
-)
 
 type NptCountItem = {
   nptName: string
@@ -62,7 +52,7 @@ function getCreateTime(type: TimeRange) {
 
 function handleProductionClick(item: NptCountItem) {
   router.push({
-    name: props.company === 'rh' ? 'IotRhDailyReport' : 'IotRdDailyReport',
+    name: 'IotRdDailyReport',
     query: {
       nptName: item.nptName,
       createTime: getCreateTime(activeTimeRange.value)
@@ -73,9 +63,7 @@ function handleProductionClick(item: NptCountItem) {
 async function loadData() {
   loading.value = true
   try {
-    const loadNptCount =
-      props.company === 'rh' ? IotStatApi.getRhNptCount : IotStatApi.getRdNptCount
-    const res = await loadNptCount({
+    const res = await IotStatApi.getRdNptCount({
       createTime: getCreateTime(activeTimeRange.value)
     })
 

+ 2 - 2
src/views/pms/stat/rhkb.vue

@@ -11,7 +11,7 @@ import rhsafeday from './rhkb/rhsafeday.vue'
 import assetValue from './rhkb/assetValue.vue'
 import equipmentRate from './rhkb/equipment-rate.vue'
 import inventorySituation from './rhkb/inventorySituation.vue'
-import exceptionPrompt from './rdkb/rd-exception-prompt.vue'
+import exceptionPrompt from './rhkb/exceptionPrompt.vue'
 
 defineOptions({
   name: 'IotRhStatt'
@@ -111,7 +111,7 @@ onUnmounted(() => {
               <equipmentRate class="kb-stage-card kb-stage-card--3" />
               <rhsafeday class="kb-stage-card kb-stage-card--4" />
               <gasStatistics class="kb-stage-card kb-stage-card--5" />
-              <exceptionPrompt company="rh" class="kb-stage-card kb-stage-card--6" />
+              <exceptionPrompt class="kb-stage-card kb-stage-card--6" />
               <assetValue class="kb-stage-card kb-stage-card--7" />
               <operation class="kb-stage-card kb-stage-card--8" />
               <inventorySituation class="kb-stage-card kb-stage-card--9" />

+ 346 - 0
src/views/pms/stat/rhkb/exceptionPrompt.vue

@@ -0,0 +1,346 @@
+<script lang="ts" setup>
+import dayjs from 'dayjs'
+import quarterOfYear from 'dayjs/plugin/quarterOfYear'
+import { IotStatApi } from '@/api/pms/stat'
+
+dayjs.extend(quarterOfYear)
+
+type TimeRange = 'day' | 'month' | 'quarter' | 'year'
+
+type NptCountItem = {
+  nptName: string
+  displayName: string
+  teamCount: number
+}
+
+const router = useRouter()
+const activeTimeRange = ref<TimeRange>('day')
+const loading = ref(false)
+const productionList = ref<NptCountItem[]>([])
+const deviceExceptionCount = ref(0)
+
+const timeOptions: Array<{ label: string; value: TimeRange }> = [
+  { label: '本日', value: 'day' },
+  { label: '本月', value: 'month' },
+  { label: '本季', value: 'quarter' },
+  { label: '全年', value: 'year' }
+]
+
+const nptNameMap: Record<string, string> = {
+  no: '无工作量',
+  xcdm: '井场待命',
+  wxz: '维修设备',
+  wz: '物资影响',
+  yc: '异常/复杂'
+}
+
+const productionTopList = computed(() => productionList.value.slice(0, 9))
+
+function getNptDisplayName(nptName?: string) {
+  if (!nptName) return '-'
+
+  return nptNameMap[nptName] || nptName
+}
+
+function getCreateTime(type: TimeRange) {
+  const now = dayjs()
+  const start = now.startOf(type === 'day' ? 'day' : type === 'year' ? 'year' : type)
+  const end = now.endOf(type === 'day' ? 'day' : type === 'year' ? 'year' : type)
+
+  return [start.format('YYYY-MM-DD HH:mm:ss'), end.format('YYYY-MM-DD HH:mm:ss')]
+}
+
+function handleProductionClick(item: NptCountItem) {
+  router.push({
+    name: 'IotRhDailyReportSummary',
+    query: {
+      activeTab: '非生产时效',
+      nptName: item.nptName,
+      createTime: getCreateTime(activeTimeRange.value)
+    }
+  })
+}
+
+async function loadData() {
+  loading.value = true
+  try {
+    const res = await IotStatApi.getRhNptCount({
+      createTime: getCreateTime(activeTimeRange.value)
+    })
+
+    productionList.value = Array.isArray(res)
+      ? res.map((item) => ({
+          nptName: item.nptName || '',
+          displayName: getNptDisplayName(item.nptName),
+          teamCount: Number(item.teamCount || 0)
+        }))
+      : []
+  } catch (error) {
+    console.error('获取瑞恒生产异常失败:', error)
+    productionList.value = []
+  } finally {
+    loading.value = false
+  }
+}
+
+watch(activeTimeRange, loadData)
+
+onMounted(loadData)
+</script>
+
+<template>
+  <div class="panel flex flex-col">
+    <div class="panel-title flex items-center justify-between">
+      <div class="kb-panel-title-text flex items-center">
+        <div class="icon-decorator">
+          <span></span>
+          <span></span>
+        </div>
+        异常警示
+      </div>
+      <el-segmented
+        v-model="activeTimeRange"
+        :options="timeOptions"
+        size="small"
+        class="exception-time-switch" />
+    </div>
+
+    <div v-loading="loading" class="exception-body">
+      <section class="exception-section exception-section--production">
+        <div class="exception-section__title">
+          <span class="exception-section__marker"></span>
+          生产异常
+          <span class="exception-section__hint">TOP 9</span>
+        </div>
+        <div class="production-grid">
+          <div
+            v-for="(item, index) in productionTopList"
+            :key="item.nptName"
+            class="production-item cursor-pointer"
+            role="button"
+            tabindex="0"
+            @click="handleProductionClick(item)"
+            @keyup.enter="handleProductionClick(item)">
+            <span class="production-item__rank">{{ index + 1 }}</span>
+            <span class="production-item__name">{{ item.displayName }}</span>
+            <span class="production-item__count">
+              <span class="production-item__value">{{ item.teamCount }}</span>
+              <span class="production-item__unit">个</span>
+            </span>
+          </div>
+          <div v-if="!productionTopList.length && !loading" class="exception-empty">
+            暂无生产异常数据
+          </div>
+        </div>
+      </section>
+
+      <section class="exception-section exception-section--device">
+        <div class="exception-section__title">
+          <span class="exception-section__marker"></span>
+          设备异常
+        </div>
+        <div class="device-exception-card">
+          <span class="device-exception-card__label">异常数量:</span>
+          <span class="device-exception-card__value">{{ deviceExceptionCount }}</span>
+          <span class="device-exception-card__unit">个</span>
+        </div>
+      </section>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+@import url('@/styles/kb.scss');
+
+.exception-time-switch {
+  --el-segmented-item-selected-color: #03409b;
+  --el-segmented-item-selected-bg-color: rgb(255 255 255 / 86%);
+  --el-segmented-bg-color: rgb(31 91 184 / 10%);
+  --el-segmented-item-hover-bg-color: rgb(255 255 255 / 56%);
+
+  min-height: calc(26px * var(--kb-scale, 1));
+  padding: calc(2px * var(--kb-scale, 1));
+  border: 1px solid rgb(31 91 184 / 12%);
+  transform: translateY(calc(-2px * var(--kb-scale, 1)));
+
+  :deep(.el-segmented__item) {
+    min-height: calc(22px * var(--kb-scale, 1));
+    padding: 0 calc(7px * var(--kb-scale, 1));
+    font-size: calc(13px * var(--kb-scale, 1));
+    font-weight: 600;
+    color: #29527f;
+  }
+}
+
+.exception-body {
+  display: flex;
+  min-height: 0;
+  flex: 1;
+  flex-direction: column;
+  gap: calc(7px * var(--kb-scale, 1));
+  padding: calc(10px * var(--kb-scale, 1)) calc(14px * var(--kb-scale, 1))
+    calc(12px * var(--kb-scale, 1));
+
+  :deep(.el-loading-mask) {
+    background-color: rgb(221 233 251 / 42%);
+  }
+}
+
+.exception-section {
+  min-height: 0;
+}
+
+.exception-section--production {
+  display: flex;
+  flex: 1 1 0;
+  flex-direction: column;
+}
+
+.exception-section--device {
+  position: relative;
+  z-index: 1;
+  flex: none;
+}
+
+.exception-section__title {
+  display: flex;
+  height: calc(19px * var(--kb-scale, 1));
+  align-items: center;
+  gap: calc(7px * var(--kb-scale, 1));
+  margin-bottom: calc(5px * var(--kb-scale, 1));
+  font-size: calc(14px * var(--kb-scale, 1));
+  font-weight: 700;
+  line-height: calc(20px * var(--kb-scale, 1));
+  color: #24364f;
+}
+
+.exception-section__marker {
+  width: calc(5px * var(--kb-scale, 1));
+  height: calc(16px * var(--kb-scale, 1));
+  background: linear-gradient(180deg, #2d7cf8 0%, #03409b 100%);
+  border-radius: 999px;
+  box-shadow: 0 0 calc(8px * var(--kb-scale, 1)) rgb(45 124 248 / 28%);
+}
+
+.exception-section__hint {
+  padding-top: calc(1px * var(--kb-scale, 1));
+  margin-left: auto;
+  font-size: calc(12px * var(--kb-scale, 1));
+  font-weight: 600;
+  color: #6f85aa;
+}
+
+.production-grid {
+  display: grid;
+  min-height: 0;
+  flex: 1;
+  padding: calc(6px * var(--kb-scale, 1));
+  gap: calc(5px * var(--kb-scale, 1)) calc(7px * var(--kb-scale, 1));
+  grid-template-columns: repeat(3, minmax(0, 1fr));
+  grid-template-rows: repeat(3, minmax(0, 1fr));
+}
+
+.production-item {
+  display: grid;
+  height: 100%;
+  min-width: 0;
+  min-height: 0;
+  align-items: center;
+  padding: 0 calc(7px * var(--kb-scale, 1));
+  overflow: hidden;
+  background: linear-gradient(180deg, rgb(255 255 255 / 58%) 0%, rgb(213 227 249 / 36%) 100%);
+  border: 1px solid rgb(255 255 255 / 74%);
+  border-radius: calc(6px * var(--kb-scale, 1));
+  box-shadow:
+    inset 0 1px 0 rgb(255 255 255 / 78%),
+    0 calc(6px * var(--kb-scale, 1)) calc(14px * var(--kb-scale, 1)) rgb(63 103 171 / 7%);
+  grid-template-columns:
+    calc(18px * var(--kb-scale, 1)) minmax(0, 1fr)
+    calc(46px * var(--kb-scale, 1));
+  gap: calc(6px * var(--kb-scale, 1));
+}
+
+.production-item__rank {
+  display: inline-flex;
+  width: calc(18px * var(--kb-scale, 1));
+  height: calc(18px * var(--kb-scale, 1));
+  align-items: center;
+  justify-content: center;
+  font-size: calc(11px * var(--kb-scale, 1));
+  font-weight: 700;
+  color: #1f5bb8;
+  background: rgb(31 91 184 / 8%);
+  border: 1px solid rgb(31 91 184 / 10%);
+  border-radius: 999px;
+}
+
+.production-item__name {
+  overflow: hidden;
+  font-size: calc(12px * var(--kb-scale, 1));
+  font-weight: 600;
+  line-height: calc(16px * var(--kb-scale, 1));
+  color: #24364f;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.production-item__count {
+  display: inline-flex;
+  align-items: baseline;
+  justify-content: flex-end;
+  gap: calc(2px * var(--kb-scale, 1));
+  min-width: 0;
+}
+
+.production-item__value {
+  font-family: YouSheBiaoTiHei, sans-serif;
+  font-size: calc(19px * var(--kb-scale, 1));
+  font-weight: 700;
+  line-height: 1;
+  color: #e43f5f;
+  text-align: right;
+}
+
+.production-item__unit {
+  font-size: calc(12px * var(--kb-scale, 1));
+  font-weight: 700;
+  color: #24364f;
+}
+
+.exception-empty {
+  grid-column: 1 / -1;
+  place-self: center;
+  font-size: calc(14px * var(--kb-scale, 1));
+  font-weight: 600;
+  color: #6f85aa;
+}
+
+.device-exception-card {
+  display: flex;
+  height: calc(42px * var(--kb-scale, 1));
+  align-items: center;
+  justify-content: center;
+  background: linear-gradient(180deg, rgb(255 255 255 / 58%) 0%, rgb(213 227 249 / 36%) 100%);
+  border: 1px solid rgb(255 255 255 / 74%);
+  border-radius: calc(6px * var(--kb-scale, 1));
+  box-shadow:
+    inset 0 1px 0 rgb(255 255 255 / 78%),
+    0 calc(6px * var(--kb-scale, 1)) calc(14px * var(--kb-scale, 1)) rgb(63 103 171 / 7%);
+}
+
+.device-exception-card__label,
+.device-exception-card__unit {
+  font-size: calc(15px * var(--kb-scale, 1));
+  font-weight: 700;
+  color: #24364f;
+}
+
+.device-exception-card__value {
+  margin: 0 calc(6px * var(--kb-scale, 1));
+  font-family: YouSheBiaoTiHei, sans-serif;
+  font-size: calc(23px * var(--kb-scale, 1));
+  font-weight: 700;
+  line-height: 1;
+  color: #e43f5f;
+}
+</style>