yanghao 1 Minggu lalu
induk
melakukan
390a0c9f3f

+ 1 - 1
src/api/pms/qhse/index.ts

@@ -301,7 +301,7 @@ export const IotHiddenApi = {
   },
   // 统计
   getHiddenStatistics: async (id) => {
-    return await request.get({ url: `/rq/iot-hazard-type?deptId=${id}` })
+    return await request.get({ url: `/rq/iot-hazard/stat?deptId=${id}` })
   }
 }
 

+ 200 - 60
src/views/pms/qhse/hazard/index.vue

@@ -37,28 +37,33 @@
       </ContentWrap>
 
       <ContentWrap style="border: 0">
-        <div v-if="staticData.length" class="stats-cards">
-          <!-- <div class="stats-card stats-card--total">
-            <div class="stats-card__label">风险总数</div>
-            <div class="stats-card__value">{{ totalRiskCount }}</div>
-          </div> -->
+        <div v-loading="!staticData.length" class="stats-cards">
           <div
-            v-for="item in staticData"
-            :key="item.classify"
+            v-for="item in statsDisplayCards"
+            :key="item.key"
             class="stats-card"
-            :class="getStatsCardClass(item.classify)">
-            <div class="stats-card__label">
-              <!-- <span
-                :class="`w-50 h-50 rounded-full inline-block bg-[${getStatsCardClass(item.classify)}]`"></span> -->
-              <span>{{ item.classify }}</span>
+            :style="getStatsCardStyle(item.accent, item.glow)">
+            <div
+              class="stats-card__decor stats-card__decor--left"
+              :style="{ background: item.glow }"></div>
+            <div
+              class="stats-card__decor stats-card__decor--right"
+              :style="{ background: item.glow }"></div>
+            <div class="stats-card__header">
+              <div class="stats-card__icon-wrap">
+                <div class="stats-card__icon" :style="{ color: item.accent }">
+                  <Icon :icon="item.icon" />
+                </div>
+              </div>
+              <div class="stats-card__title">{{ item.label }}</div>
             </div>
-            <div class="stats-card__value">
+            <div class="stats-card__body">
               <CountTo
                 :duration="2600"
                 :end-val="item.count"
                 :start-val="0"
-                :class="'stats-card__count'"
-                :style="{ color: getStatsCardClass(item.classify) }" />
+                class="stats-card__count"
+                :style="{ color: item.accent }" />
             </div>
           </div>
         </div>
@@ -68,7 +73,7 @@
           style="width: 100%"
           :header-cell-style="{ background: '#f5f7fa', color: '#333' }"
           :cell-style="{ padding: '12px 8px' }"
-          height="55vh">
+          height="52.7vh">
           <!-- 区域/位置 列(已合并) -->
           <zm-table-column prop="region" label="区域/位置" align="center" fixed="left" />
 
@@ -536,6 +541,63 @@ const totalRiskCount = computed(() =>
   staticData.value.reduce((sum, item) => sum + (Number(item.count) || 0), 0)
 )
 
+const getStatsCardMeta = (classify) => {
+  const value = String(classify || '')
+  if (value.includes('重大') || value.includes('閲嶅ぇ')) {
+    return {
+      accent: '#ff5b61',
+      glow: 'radial-gradient(circle, rgba(255, 91, 97, 0.22) 0%, rgba(255, 91, 97, 0) 72%)',
+      icon: 'ep:warning-filled'
+    }
+  }
+  if (value.includes('较大') || value.includes('杈冨ぇ')) {
+    return {
+      accent: '#ff9827',
+      glow: 'radial-gradient(circle, rgba(255, 152, 39, 0.24) 0%, rgba(255, 152, 39, 0) 72%)',
+      icon: 'ep:opportunity'
+    }
+  }
+  if (value.includes('一般') || value.includes('涓€鑸')) {
+    return {
+      accent: '#3d7cff',
+      glow: 'radial-gradient(circle, rgba(61, 124, 255, 0.2) 0%, rgba(61, 124, 255, 0) 72%)',
+      icon: 'ep:info-filled'
+    }
+  }
+  if (value.includes('低') || value.includes('浣')) {
+    return {
+      accent: '#25b36a',
+      glow: 'radial-gradient(circle, rgba(37, 179, 106, 0.22) 0%, rgba(37, 179, 106, 0) 72%)',
+      icon: 'ep:success-filled'
+    }
+  }
+  return {
+    accent: '#5f7da8',
+    glow: 'radial-gradient(circle, rgba(95, 125, 168, 0.18) 0%, rgba(95, 125, 168, 0) 72%)',
+    icon: 'ep:data-analysis'
+  }
+}
+
+const statsDisplayCards = computed(() =>
+  staticData.value.map((item, index) => {
+    const meta = getStatsCardMeta(item.classify)
+    const count = Number(item.count) || 0
+    const rate = totalRiskCount.value ? ((count / totalRiskCount.value) * 100).toFixed(1) : '0.0'
+    return {
+      key: `${item.classify}-${index}`,
+      label: item.classify,
+      count,
+      note: `占比:${rate}%`,
+      ...meta
+    }
+  })
+)
+
+const getStatsCardStyle = (accent, glow) => ({
+  '--stats-accent': accent,
+  '--stats-glow': glow
+})
+
 const getStatsCardClass = (classify) => {
   const value = String(classify || '')
   if (value.includes('重大')) return 'stats-card--major'
@@ -613,75 +675,153 @@ onMounted(() => {
 
 .stats-cards {
   display: grid;
-  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
+  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
   gap: 12px;
   margin-bottom: 16px;
 }
 
 .stats-card {
-  padding: 16px;
-  border-radius: 10px;
-  border: 1px solid #e4ecf7;
-
-  background: linear-gradient(180deg, rgb(212 228 252 / 42%) 0%, rgb(220 232 250 / 28%) 100%);
-  border: 1px solid rgb(255 255 255 / 58%);
-  border-radius: 18px;
+  position: relative;
+  overflow: hidden;
+  min-height: 132px;
+  padding: 18px 18px 16px;
+  border-radius: 22px;
+  background: radial-gradient(
+      circle at 18% 22%,
+      rgb(255 255 255 / 92%) 0%,
+      rgb(255 255 255 / 0%) 20%
+    ),
+    radial-gradient(circle at 88% 80%, rgb(255 215 158 / 22%) 0%, rgb(255 215 158 / 0%) 16%),
+    linear-gradient(135deg, rgb(239 245 255 / 96%) 0%, rgb(217 230 248 / 88%) 100%);
+  border: 1px solid rgb(255 255 255 / 62%);
   box-shadow:
-    inset 0 1px 0 rgb(255 255 255 / 74%),
-    0 8px 18px rgb(63 103 171 / 7%);
+    inset 0 1px 0 rgb(255 255 255 / 86%),
+    0 14px 30px rgb(116 146 191 / 12%);
 }
 
-.stats-card--total {
-  background: linear-gradient(180deg, #eff7ff 0%, #dfefff 100%);
-  border-color: #bfd8fb;
+.stats-card__decor {
+  position: absolute;
+  border-radius: 999px;
+  pointer-events: none;
+  filter: blur(8px);
+  opacity: 0.95;
 }
 
-.stats-card__label {
-  font-size: 14px;
-  font-weight: 600;
-  color: #6b7280;
+.stats-card__decor--left {
+  width: 72px;
+  height: 72px;
+  left: -10px;
+  top: -8px;
 }
 
-.stats-card__value {
-  margin-top: 10px;
+.stats-card__decor--right {
+  width: 88px;
+  height: 88px;
+  right: -18px;
+  bottom: -24px;
 }
 
-.stats-card__count {
-  display: block;
-  padding-top: 10px;
-  text-align: center;
-  font-size: 40px !important;
-  font-weight: 700;
-  line-height: 1;
+.stats-card__header {
+  position: relative;
+  z-index: 1;
+  display: flex;
+  align-items: center;
+  gap: 14px;
+}
+
+.stats-card__icon-wrap {
+  width: 48px;
+  height: 48px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  border-radius: 14px;
+  box-shadow:
+    inset 0 1px 0 rgb(255 255 255 / 85%),
+    0 10px 24px rgb(118 144 187 / 10%);
 }
 
-.stats-card--major .stats-card__label,
-.stats-card--major .stats-card__count {
-  color: #d03050;
+.stats-card__icon {
+  font-size: 24px;
+  /* line-height: 1; */
 }
 
-.stats-card--high .stats-card__label,
-.stats-card--high .stats-card__count {
-  color: #dd6b20;
+.stats-card__title {
+  font-size: 16px;
+  font-weight: 700;
+  color: #324b72;
+  letter-spacing: 0;
 }
 
-.stats-card--medium .stats-card__label,
-.stats-card--medium .stats-card__count {
-  color: #ebaa3c;
+.stats-card__body {
+  position: relative;
+  z-index: 1;
+  display: flex;
+  align-items: flex-end;
+  gap: 8px;
+  margin-top: 14px;
+  padding-left: 62px;
 }
 
-.stats-card--low .stats-card__label,
-.stats-card--low .stats-card__count {
-  color: #f2c11a;
+.stats-card__count {
+  display: block;
+  font-size: 38px !important;
+  font-weight: 800;
+  line-height: 0.92;
+  letter-spacing: 1px;
+  font-style: italic;
+  text-shadow: 0 8px 18px rgb(68 110 183 / 10%);
 }
 
-.stats-card--default .stats-card__label,
-.stats-card--default .stats-card__count {
-  color: #475569;
+.stats-card__note {
+  padding-bottom: 4px;
+  font-size: 16px;
+  font-weight: 700;
+  line-height: 1;
 }
 
-.stats-card--total .stats-card__label,
-.stats-card--total .stats-card__value {
-  color: #1f5bb8;
+@media (max-width: 768px) {
+  .stats-cards {
+    grid-template-columns: 1fr;
+  }
+
+  .stats-card {
+    min-height: 160px;
+    padding: 24px 22px 22px;
+    border-radius: 22px;
+  }
+
+  .stats-card__header {
+    gap: 18px;
+  }
+
+  .stats-card__icon-wrap {
+    width: 58px;
+    height: 58px;
+    border-radius: 16px;
+  }
+
+  .stats-card__icon {
+    font-size: 28px;
+  }
+
+  .stats-card__title {
+    font-size: 17px;
+  }
+
+  .stats-card__body {
+    margin-top: 18px;
+    padding-left: 76px;
+    gap: 10px;
+  }
+
+  .stats-card__count {
+    font-size: 46px !important;
+  }
+
+  .stats-card__note {
+    font-size: 18px;
+    padding-bottom: 6px;
+  }
 }
 </style>

+ 248 - 0
src/views/pms/qhse/safety/index.vue

@@ -57,6 +57,78 @@
 
       <!-- 列表 -->
       <ContentWrap class="flex-1 overflow-hidden mt-15px" style="border: none">
+        <div class="stats-cards">
+          <div class="stats-card">
+            <div
+              class="stats-card__decor stats-card__decor--left"
+              :style="{
+                background:
+                  'radial-gradient(circle, rgba(61, 124, 255, 0.2) 0%, rgba(61, 124, 255, 0) 72%)'
+              }"></div>
+            <div class="stats-card__decor stats-card__decor--right"></div>
+            <div class="stats-card__header">
+              <div class="stats-card__icon-wrap">
+                <div class="stats-card__icon">
+                  <Icon icon="eos-icons:counting" color="#2563eb" />
+                </div>
+              </div>
+              <div class="stats-card__title">隐患总数</div>
+            </div>
+            <div class="stats-card__body">
+              <CountTo
+                :duration="2600"
+                :end-val="hiddenCount"
+                :start-val="0"
+                class="stats-card__count text-2xl text-center! text-[#2563eb]!" />
+            </div>
+          </div>
+
+          <div class="stats-card">
+            <div
+              class="stats-card__decor stats-card__decor--left"
+              :style="{
+                background:
+                  'radial-gradient(circle, rgba(255, 91, 97, 0.22) 0%, rgba(255, 91, 97, 0) 72%)'
+              }"></div>
+            <div class="stats-card__decor stats-card__decor--right"></div>
+            <div class="stats-card__header">
+              <div class="stats-card__icon-wrap">
+                <div class="stats-card__icon" :style="{ color: '#ff5b61' }">
+                  <Icon icon="ep:info-filled" color="#de3b3b" />
+                </div>
+              </div>
+              <div class="stats-card__title">未整改数</div>
+            </div>
+            <div class="stats-card__body">
+              <CountTo
+                :duration="2600"
+                :end-val="todo"
+                :start-val="0"
+                class="stats-card__count text-2xl text-center! text-[#ff5b61]!" />
+            </div>
+          </div>
+
+          <div class="stats-card">
+            <div class="stats-card__decor stats-card__decor--left"></div>
+            <div class="stats-card__decor stats-card__decor--right"></div>
+            <div class="stats-card__header">
+              <div class="stats-card__icon-wrap">
+                <div class="stats-card__icon">
+                  <Icon icon="ep:info-filled" color="#de3b3b" />
+                </div>
+              </div>
+              <div class="stats-card__title">整改率</div>
+            </div>
+            <div class="stats-card__body">
+              <CountTo
+                :duration="2600"
+                :end-val="((hiddenCount - todo) / hiddenCount) * 100"
+                :start-val="0"
+                class="stats-card__count text-2xl text-center! text-[#2563eb]!" />
+              <span class="text-[#2563eb]!">%</span>
+            </div>
+          </div>
+        </div>
         <zm-table
           :loading="loading"
           :data="list"
@@ -311,6 +383,10 @@ import UploadFile from '@/components/UploadFile/src/UploadFile.vue'
 import FilePreviewDialog from '@/components/FilePreview/src/FilePreviewDialog.vue'
 import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
 
+import { useUserStore } from '@/store/modules/user'
+
+const userStore = useUserStore()
+
 import { useTableComponents } from '@/components/ZmTable/useTableComponents'
 const { ZmTable, ZmTableColumn } = useTableComponents()
 
@@ -407,6 +483,7 @@ const handleExport = async () => {
 const handleDeptNodeClick = async (row) => {
   queryParams.deptId = row.id
   await getList()
+  await getStatic()
 }
 
 /** 搜索按钮操作 */
@@ -623,9 +700,28 @@ const getTree = async () => {
   deptTree.value.push(dept)
   // firstLevelKeys.value = deptTree.value.map((node) => node.id)
 }
+
+let hiddenCount = ref(0)
+let todo = ref(0)
+let source = ref('')
+async function getStatic() {
+  if (queryParams.deptId) {
+    const res = await IotHiddenApi.getHiddenStatistics(queryParams.deptId)
+    hiddenCount.value = res.total
+    todo.value = res.todo
+    source.value = res.source
+  } else {
+    const res = await IotHiddenApi.getHiddenStatistics(userStore.user.deptId)
+    hiddenCount.value = res.total
+    todo.value = res.todo
+    source.value = res.source
+  }
+}
 onMounted(async () => {
   getList()
   deptList2.value = handleTree(await DeptApi.getSimpleDeptList())
+
+  getStatic()
 })
 </script>
 
@@ -641,4 +737,156 @@ onMounted(async () => {
   display: flex;
   justify-content: center;
 }
+
+.stats-cards {
+  display: grid;
+  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
+  gap: 12px;
+  margin-bottom: 16px;
+}
+
+.stats-card {
+  position: relative;
+  overflow: hidden;
+  min-height: 132px;
+  padding: 18px 18px 16px;
+  border-radius: 22px;
+  background: radial-gradient(
+      circle at 18% 22%,
+      rgb(255 255 255 / 92%) 0%,
+      rgb(255 255 255 / 0%) 20%
+    ),
+    radial-gradient(circle at 88% 80%, rgb(255 215 158 / 22%) 0%, rgb(255 215 158 / 0%) 16%),
+    linear-gradient(135deg, rgb(239 245 255 / 96%) 0%, rgb(217 230 248 / 88%) 100%);
+  border: 1px solid rgb(255 255 255 / 62%);
+  box-shadow:
+    inset 0 1px 0 rgb(255 255 255 / 86%),
+    0 14px 30px rgb(116 146 191 / 12%);
+}
+
+.stats-card__decor {
+  position: absolute;
+  border-radius: 999px;
+  pointer-events: none;
+  filter: blur(8px);
+  opacity: 0.95;
+}
+
+.stats-card__decor--left {
+  width: 72px;
+  height: 72px;
+  left: -10px;
+  top: -8px;
+}
+
+.stats-card__decor--right {
+  width: 88px;
+  height: 88px;
+  right: -18px;
+  bottom: -24px;
+}
+
+.stats-card__header {
+  position: relative;
+  z-index: 1;
+  display: flex;
+  align-items: center;
+  gap: 14px;
+}
+
+.stats-card__icon-wrap {
+  width: 48px;
+  height: 48px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  border-radius: 14px;
+  box-shadow:
+    inset 0 1px 0 rgb(255 255 255 / 85%),
+    0 10px 24px rgb(118 144 187 / 10%);
+}
+
+.stats-card__icon {
+  font-size: 24px;
+  /* line-height: 1; */
+}
+
+.stats-card__title {
+  font-size: 16px;
+  font-weight: 700;
+  color: #324b72;
+  letter-spacing: 0;
+}
+
+.stats-card__body {
+  position: relative;
+  z-index: 1;
+  display: flex;
+  align-items: flex-end;
+  gap: 8px;
+  margin-top: 14px;
+  padding-left: 62px;
+}
+
+.stats-card__count {
+  display: block;
+  font-size: 38px !important;
+  font-weight: 800;
+  line-height: 0.92;
+  letter-spacing: 1px;
+  font-style: italic;
+  text-shadow: 0 8px 18px rgb(68 110 183 / 10%);
+}
+
+.stats-card__note {
+  padding-bottom: 4px;
+  font-size: 16px;
+  font-weight: 700;
+  line-height: 1;
+}
+
+@media (max-width: 768px) {
+  .stats-cards {
+    grid-template-columns: 1fr;
+  }
+
+  .stats-card {
+    min-height: 160px;
+    padding: 24px 22px 22px;
+    border-radius: 22px;
+  }
+
+  .stats-card__header {
+    gap: 18px;
+  }
+
+  .stats-card__icon-wrap {
+    width: 58px;
+    height: 58px;
+    border-radius: 16px;
+  }
+
+  .stats-card__icon {
+    font-size: 28px;
+  }
+
+  .stats-card__title {
+    font-size: 17px;
+  }
+
+  .stats-card__body {
+    margin-top: 18px;
+    padding-left: 76px;
+    gap: 10px;
+  }
+
+  .stats-card__count {
+    font-size: 46px !important;
+  }
+
+  .stats-card__note {
+    font-size: 18px;
+    padding-bottom: 6px;
+  }
+}
 </style>