فهرست منبع

台账信息财务信息

yanghao 5 روز پیش
والد
کامیت
737d1bb50b
38فایلهای تغییر یافته به همراه4373 افزوده شده و 319 حذف شده
  1. 1 1
      .env.local
  2. 1 0
      .gitignore
  3. 43 0
      src/api/pms/iotryimprovedailyreport/index.ts
  4. 25 4
      src/api/pms/stat/index.ts
  5. 8 16
      src/components/DeptTreeSelect/index.vue
  6. 1 1
      src/config/axios/service.ts
  7. 48 29
      src/layout/components/Menu/src/components/useRenderMenuItem.tsx
  8. 9 1
      src/styles/kb.scss
  9. 1 1
      src/utils/kb.ts
  10. 91 100
      src/views/Home/Index.vue
  11. 116 0
      src/views/Home/stat.vue
  12. 39 2
      src/views/pms/iotrhdailyreport/components/DailyStatistics.vue
  13. 50 0
      src/views/pms/iotrhdailyreport/summary.vue
  14. 369 0
      src/views/pms/iotrydailyreport/components/equipment-form.vue
  15. 236 0
      src/views/pms/iotrydailyreport/equipment.vue
  16. 16 8
      src/views/pms/operation-meeting/components/operation-meeting-content.vue
  17. 109 13
      src/views/pms/stat/rhkb.vue
  18. 190 0
      src/views/pms/stat/rhkb/assetValue.vue
  19. 38 3
      src/views/pms/stat/rhkb/deviceList.vue
  20. 41 13
      src/views/pms/stat/rhkb/deviceStatus.vue
  21. 1 1
      src/views/pms/stat/rhkb/deviceType.vue
  22. 244 0
      src/views/pms/stat/rhkb/equipment-rate.vue
  23. 35 1
      src/views/pms/stat/rhkb/historyGas.vue
  24. 507 0
      src/views/pms/stat/rhkb/inventorySituation.vue
  25. 29 0
      src/views/pms/stat/rhkb/operation.vue
  26. 163 0
      src/views/pms/stat/rhkb/rhsafeday.vue
  27. 149 32
      src/views/pms/stat/rhkb/rhsummary.vue
  28. 36 1
      src/views/pms/stat/rhkb/todayGas.vue
  29. 17 10
      src/views/pms/stat/rykb.vue
  30. 397 0
      src/views/pms/stat/rykb/equipment-category.vue
  31. 318 0
      src/views/pms/stat/rykb/exception-prompt.vue
  32. 347 0
      src/views/pms/stat/rykb/historical-workload.vue
  33. 557 0
      src/views/pms/stat/rykb/inventory-situation.vue
  34. 86 48
      src/views/pms/stat/rykb/rysummary.vue
  35. 1 1
      src/views/pms/stat/rykb/safeday.vue
  36. 2 1
      src/views/pms/stat/rykb/zjfinish.vue
  37. 2 1
      src/views/pms/stat/rykb/zjwork.vue
  38. 50 31
      src/views/report-statistics/costs.vue

+ 1 - 1
.env.local

@@ -3,7 +3,7 @@ NODE_ENV=development
 
 VITE_DEV=true
 
-# 请求路径  http://192.168.188.79:48080  https://iot.deepoil.cc  http://172.26.0.56:48080
+# 请求路径  http://192.168.188.200:48080  https://iot.deepoil.cc  http://172.26.0.56:48080
 VITE_BASE_URL='https://iot.deepoil.cc'
 
 # 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持 S3 服务

+ 1 - 0
.gitignore

@@ -7,3 +7,4 @@ pnpm-debug
 auto-*.d.ts
 .idea
 .history
+pnpm-workspace.yaml

+ 43 - 0
src/api/pms/iotryimprovedailyreport/index.ts

@@ -0,0 +1,43 @@
+import request from '@/config/axios'
+
+export interface IotRyImproveDailyReportVO {
+  id?: number
+  title: string
+  createTime: number
+  workLocation: string
+  workPurpose: string
+  relocationDays: number
+  productionStatus: string
+  personnel: string
+  nextPlan: string
+  auditStatus?: 0 | 10 | 20 | 30 | 40
+  opinion?: string
+}
+
+export interface IotRyImproveDailyReportApprovalVO {
+  id: number
+  auditStatus: 20 | 30
+  opinion?: string
+}
+
+export const IotRyImproveDailyReportApi = {
+  getIotRyImproveDailyReportPage: async (params: any) => {
+    return await request.get({ url: `/pms/iot-ry-improve-daily-report/page`, params })
+  },
+
+  getIotRyImproveDailyReport: async (id: number) => {
+    return await request.get({ url: `/pms/iot-ry-improve-daily-report/get?id=` + id })
+  },
+
+  createIotRyImproveDailyReport: async (data: IotRyImproveDailyReportVO) => {
+    return await request.post({ url: `/pms/iot-ry-improve-daily-report/create`, data })
+  },
+
+  updateIotRyImproveDailyReport: async (data: IotRyImproveDailyReportVO) => {
+    return await request.put({ url: `/pms/iot-ry-improve-daily-report/update`, data })
+  },
+
+  approvalIotRyImproveDailyReport: async (data: IotRyImproveDailyReportApprovalVO) => {
+    return await request.put({ url: `/pms/iot-ry-improve-daily-report/approval`, data })
+  }
+}

+ 25 - 4
src/api/pms/stat/index.ts

@@ -21,6 +21,12 @@ export const IotStatApi = {
   getRepairRigWork: async (params: any) => {
     return await request.get({ url: `/rq/stat/ry/dailyReport/` + params })
   },
+  getRyHiZjDailyReports: async () => {
+    return await request.get({ url: `/rq/stat/ry/hiZjDailyReports` })
+  },
+  getRyHiXjDailyReports: async () => {
+    return await request.get({ url: `/rq/stat/ry/hiXjDailyReports` })
+  },
   getOrderYwcb: async (params: any) => {
     return await request.get({ url: `/rq/stat/rh/ywcb/` + params })
   },
@@ -57,8 +63,11 @@ export const IotStatApi = {
   getInspectStatuss: async (params: any, dept: any) => {
     return await request.get({ url: `/rq/stat/inspect/statuss/` + dept, params })
   },
-  getInspectZjxjCount: async () => {
-    return await request.get({ url: `/rq/stat/home/ry/count/zjxj` })
+  getInspectZjxjCount: async (params?: any) => {
+    return await request.get({ url: `/rq/stat/home/ry/count/zjxj`, params })
+  },
+  getInspectRate: async (params: any) => {
+    return await request.get({ url: `/rq/stat/ry/device/totalUtilizationRate`, params })
   },
   getProject: async (params: any) => {
     return await request.get({ url: `/rq/stat/project/` + params })
@@ -90,6 +99,9 @@ export const IotStatApi = {
   getRhZql: async (params: any) => {
     return await request.get({ url: `/rq/stat/year/total/gas/` + params })
   },
+  getRhZqlTotal: async (params: any) => {
+    return await request.get({ url: `/rq/stat/rh/zql`, params })
+  },
   getRhZqlGases: async (params: any) => {
     return await request.get({ url: `/rq/stat/year/total/gases/` + params })
   },
@@ -147,6 +159,12 @@ export const IotStatApi = {
   getRhRate: async (params: any) => {
     return await request.get({ url: `/rq/stat/rh/device/utilizationRate`, params })
   },
+  getRhTotalUtilizationRate: async (params: any) => {
+    return await request.get({ url: `/rq/stat/rh/device/totalUtilizationRate`, params })
+  },
+  getRhSevenDayUtilization: async () => {
+    return await request.get({ url: `/rq/stat/rh/device/sevenDayUtilization` })
+  },
   getRyRate: async (params: any) => {
     return await request.get({ url: `/rq/stat/ry/device/utilizationRate`, params })
   },
@@ -162,6 +180,9 @@ export const IotStatApi = {
   getRyProductionBriefs: async (params: any) => {
     return await request.get({ url: `/pms/iot-ry-daily-report/productionBriefs`, params })
   },
+  getRyNptCount: async (params: any) => {
+    return await request.get({ url: `/rq/stat/ry/device/nptCount`, params })
+  },
   getRdTeamRate: async (params: any) => {
     return await request.get({ url: `rq/stat/rd/device/teamUtilizationRate`, params })
   },
@@ -187,8 +208,8 @@ export const IotStatApi = {
   getSafeCount: async () => {
     return await request.get({ url: `/rq/stat/home/safe` })
   },
-  getSafeCount1: async () => {
-    return await request.get({ url: `/rq/qhse-safe-day/get/dept/` + 158, params: { deptId: 158 } })
+  getSafeCount1: async (deptId: number) => {
+    return await request.get({ url: `/rq/qhse-safe-day/get/dept/` + deptId, params: { deptId } })
   },
   getMttr: async () => {
     return await request.get({ url: `/rq/stat/mttr` })

+ 8 - 16
src/components/DeptTreeSelect/index.vue

@@ -75,22 +75,17 @@ const loadDeptData = async () => {
 const loadTree = async () => {
   try {
     const { depts, currentId } = await loadDeptData()
-    if (
-      props.initSelect &&
-      props.modelValue &&
-      currentId !== undefined &&
-      !depts.some((item) => item.id === props.modelValue)
-    ) {
+    const targetKey = props.modelValue ?? (props.initSelect ? currentId : null)
+    if (props.initSelect && props.modelValue) {
       emits('update:modelValue', currentId)
     }
+    if (targetKey && !expandedKeys.value.includes(targetKey)) {
+      expandedKeys.value = [...expandedKeys.value, targetKey]
+    }
     deptList.value = sortTreeBySort(handleTree(depts))
     nextTick(() => {
-      const targetKey = props.modelValue ?? (props.initSelect ? currentId : null)
       if (targetKey && treeRef.value) {
         treeRef.value.setCurrentKey(targetKey)
-        if (!expandedKeys.value.includes(targetKey)) expandedKeys.value.push(targetKey)
-        else if (deptList.value.length > 0)
-          expandedKeys.value = deptList.value.map((item) => item.id)
       }
     })
   } catch (e) {
@@ -125,8 +120,7 @@ onMounted(loadTree)
 <template>
   <div
     class="dept-aside-container relative bg-white dark:bg-[#1d1e1f] shadow rounded-lg transition-all duration-300 ease-in-out overflow-visible"
-    :class="[isCollapsed ? 'is-collapsed' : 'p-4']"
-  >
+    :class="[isCollapsed ? 'is-collapsed' : 'p-4']">
     <div v-show="!isCollapsed" class="h-full flex flex-col gap-4 overflow-hidden w-full">
       <h1 v-if="showTitle" class="text-lg font-medium truncate shrink-0">{{ props.title }}</h1>
 
@@ -136,8 +130,7 @@ onMounted(loadTree)
           placeholder="请输入部门名称"
           clearable
           size="default"
-          :prefix-icon="Search"
-        />
+          :prefix-icon="Search" />
       </div>
 
       <div class="flex-1 relative overflow-hidden">
@@ -153,8 +146,7 @@ onMounted(loadTree)
                 node-key="id"
                 highlight-current
                 :default-expanded-keys="expandedKeys"
-                @node-click="handleNodeClick"
-              />
+                @node-click="handleNodeClick" />
             </el-scrollbar>
           </template>
         </el-auto-resizer>

+ 1 - 1
src/config/axios/service.ts

@@ -217,7 +217,7 @@ service.interceptors.response.use(
       ) {
         const localeStore = useLocaleStore()
         const lang = localeStore.getCurrentLocale.lang
-        if (data && data.data) {
+        if (data && data.data !== null && data.data !== undefined) {
           if (data.data.list) {
             if (Array.isArray(data.data.list)) {
               const list = langHelper.transformArray(data.data.list, lang)

+ 48 - 29
src/layout/components/Menu/src/components/useRenderMenuItem.tsx

@@ -53,27 +53,35 @@ export const useRenderMenuItem = () =>
             return (
               !v.meta?.hidden &&
               [
-                'QHSE',
+                'QHSE驾驶舱',
                 'QHSE看板',
-                '特种作业人员',
-                '证书人员',
-                '证书管理',
-                '计量器具',
-                '台账管理',
-                '仪器检测',
-                '使用记录',
-                '证书管理',
+                '质量管控',
+                '计量器具管理',
+                '计量器具台账',
+                '检测证书管控',
+                '安全风控',
+                '行为安全管理',
+                '分析数据源',
+                '安全行为观察',
+                '工艺安全管理',
                 '危险源辨识',
-                '事故事件上报',
-                '环境因素识别',
+                '工作安全分析',
+                '高危作业许可',
+                '系统安全管理',
+                '特种作业人员',
+                '人员台账',
+                '人员证书',
                 '隐患排查',
                 '隐患排查分类',
                 '隐患排查记录',
-                'SOC管理',
-                'SOC卡分析数据源',
-                'SOC卡汇总',
-                'JSA',
-                'PTW'
+                '应急体系',
+                '突发事件应急处理',
+                '环境合规',
+                '环境因素识别',
+                '职业健康',
+                '劳保用品',
+                '职业健康体检',
+                '健康培训'
               ].includes(v.meta?.title)
             )
           } else if (currentSource === 'yyhy') {
@@ -84,24 +92,35 @@ export const useRenderMenuItem = () =>
             return (
               !v.meta?.hidden &&
               [
-                'QHSE',
+                'QHSE驾驶舱',
                 'QHSE看板',
-                '计量器具',
-                '台账管理',
-                '仪器检测',
-                '使用记录',
-                '证书管理',
+                '质量管控',
+                '计量器具管理',
+                '计量器具台账',
+                '检测证书管控',
+                '安全风控',
+                '行为安全管理',
+                '分析数据源',
+                '安全行为观察',
+                '工艺安全管理',
                 '危险源辨识',
-                '事故事件上报',
-                '环境因素识别',
+                '工作安全分析',
+                '高危作业许可',
+                '系统安全管理',
+                '特种作业人员',
+                '人员台账',
+                '人员证书',
                 '隐患排查',
                 '隐患排查分类',
                 '隐患排查记录',
-                'SOC管理',
-                'SOC卡分析数据源',
-                'SOC卡汇总',
-                'JSA',
-                'PTW'
+                '应急体系',
+                '突发事件应急处理',
+                '环境合规',
+                '环境因素识别',
+                '职业健康',
+                '劳保用品',
+                '职业健康体检',
+                '健康培训'
               ].includes(v.meta?.title)
             )
           } else {

+ 9 - 1
src/styles/kb.scss

@@ -614,7 +614,15 @@
 }
 
 .kb-stage-card--8 {
-  --panel-delay: 0.46s;
+  --panel-delay: 0.44s;
+}
+
+.kb-stage-card--9 {
+  --panel-delay: 0.5s;
+}
+
+.kb-stage-card--10 {
+  --panel-delay: 0.56s;
 }
 
 @keyframes kb-panel-enter {

+ 1 - 1
src/utils/kb.ts

@@ -122,7 +122,7 @@ export function createLegend(extra: Partial<LegendOption> = {}, data: string[] =
     data,
     textStyle: {
       color: THEME.text.regular,
-      fontSize: 13,
+      fontSize: 10,
       fontFamily: FONT_FAMILY
     },
     ...extra

+ 91 - 100
src/views/Home/Index.vue

@@ -1,116 +1,107 @@
 <script lang="ts" setup>
-import { DESIGN_HEIGHT, DESIGN_WIDTH } from '@/utils/kb'
-import hsummary from './kb/hsummary.vue'
-import hdeviceType from './kb/hdeviceType.vue'
-import dayfinish from './kb/dayfinish.vue'
-import hsafe from './kb/hsafe.vue'
-import hdeviceStatus from './kb/hdeviceStatus.vue'
-import horderTrend from './kb/horderTrend.vue'
-
-const company = ref('首页')
-
-const wrapperRef = ref<HTMLDivElement>()
-const scale = ref(1)
-
-let resizeObserver: ResizeObserver | null = null
-let resizeRaf = 0
-
-provide('homeKbScale', scale)
-
-const targetWrapperStyle = computed(() => ({
-  width: `${DESIGN_WIDTH * scale.value}px`,
-  height: `${DESIGN_HEIGHT * scale.value}px`
-}))
-
-const targetAreaStyle = computed(() => {
-  return {
-    '--kb-scale': scale.value,
-    width: `${DESIGN_WIDTH * scale.value}px`,
-    height: `${DESIGN_HEIGHT * scale.value}px`
-  }
-})
-
-function updateScale() {
-  cancelAnimationFrame(resizeRaf)
-
-  resizeRaf = requestAnimationFrame(() => {
-    const wrapper = wrapperRef.value
-    if (!wrapper) return
-
-    const { clientWidth, clientHeight } = wrapper
-    if (!clientWidth || !clientHeight) return
-
-    scale.value = Math.min(clientWidth / DESIGN_WIDTH, clientHeight / DESIGN_HEIGHT)
-    nextTick(() => {
-      window.dispatchEvent(new Event('homekb:resize'))
-    })
-  })
+import { IotOpeationFillApi } from '@/api/pms/iotopeationfill'
+import { IotStatApi } from '@/api/pms/stat'
+import { useUserStore } from '@/store/modules/user'
+import Rhkb from '@/views/pms/stat/rhkb.vue'
+import Rykb from '@/views/pms/stat/rykb.vue'
+import Stat from './stat.vue'
+
+const user = useUserStore().getUser
+
+const company = ref<string>('')
+const companyLoading = ref(true)
+const iframeUrl = ref('')
+const iframeLoading = ref(false)
+let iframeRequestId = 0
+
+const reportUrls: Record<string, string> = {
+  rd: 'https://report.deepoil.cc/webroot/decision/v10/entry/access/a12df128-c84f-44be-a55d-bababbf4a132?preview=true&page_number=1',
+  jt: 'https://report.deepoil.cc/webroot/decision/v10/entry/access/dbc9cf73-81ce-43f1-9923-45cdfa5d5d3a?preview=true&page_number=1'
 }
 
-onMounted(() => {
-  nextTick(updateScale)
-  resizeObserver = new ResizeObserver(updateScale)
-  if (wrapperRef.value) {
-    resizeObserver.observe(wrapperRef.value)
-  }
-  window.addEventListener('resize', updateScale)
+const currentCompany = computed(() => company.value?.toLowerCase())
+const currentView = computed(() => {
+  if (companyLoading.value) return 'loading'
+  if (currentCompany.value === 'rh') return 'rh'
+  if (currentCompany.value === 'ry') return 'ry'
+  if (currentCompany.value && reportUrls[currentCompany.value]) return 'report'
+  return 'stat'
 })
 
-onUnmounted(() => {
-  resizeObserver?.disconnect()
-  window.removeEventListener('resize', updateScale)
-  cancelAnimationFrame(resizeRaf)
-})
+watch(
+  () => user.deptId,
+  async (val) => {
+    companyLoading.value = true
+    try {
+      if (!val) {
+        company.value = ''
+        return
+      }
+      company.value = await IotOpeationFillApi.getOrgName(val)
+    } finally {
+      companyLoading.value = false
+    }
+  },
+  { immediate: true }
+)
+
+watch(
+  currentCompany,
+  async (val) => {
+    const requestId = ++iframeRequestId
+    iframeUrl.value = ''
+    const reportUrl = val ? reportUrls[val] : ''
+    if (!reportUrl) {
+      iframeLoading.value = false
+      return
+    }
+
+    iframeLoading.value = true
+    try {
+      const token = await IotStatApi.getSsoToken()
+      if (requestId !== iframeRequestId) return
+      iframeUrl.value = `${reportUrl}&ssoToken=${token}`
+    } finally {
+      if (requestId === iframeRequestId) {
+        iframeLoading.value = false
+      }
+    }
+  },
+  { immediate: true }
+)
 </script>
 <template>
-  <div ref="wrapperRef" class="bg absolute top-0 left-0 size-full z-10">
-    <div class="mx-a overflow-hidden" :style="targetWrapperStyle">
-      <div class="bg kb-screen" :style="targetAreaStyle">
-        <header class="header">{{ company }}</header>
-        <div class="kb-content">
-          <hsummary class="kb-stage-card kb-stage-card--1" />
-          <div class="home-top-grid">
-            <!-- <hmttr class="kb-stage-card kb-stage-card--2" /> -->
-            <hsafe class="row-span-2 kb-stage-card kb-stage-card--3" />
-            <hdeviceType class="row-span-2 kb-stage-card kb-stage-card--4" />
-            <dayfinish class="row-span-2 kb-stage-card kb-stage-card--5" />
-          </div>
-          <div class="home-bottom-grid">
-            <hdeviceStatus class="kb-stage-card kb-stage-card--6" />
-            <horderTrend class="kb-stage-card kb-stage-card--7" />
-          </div>
-        </div>
-      </div>
-    </div>
+  <div v-if="currentView === 'loading'" class="full-screen-container" v-loading="true"></div>
+  <div v-else-if="currentView === 'report'" class="full-screen-container" v-loading="iframeLoading">
+    <iframe
+      v-if="iframeUrl"
+      :src="iframeUrl"
+      frameborder="0"
+      class="full-screen-iframe"
+      allowfullscreen></iframe>
   </div>
+  <Rhkb v-else-if="currentView === 'rh'" />
+  <Rykb v-else-if="currentView === 'ry'" />
+  <Stat v-else />
 </template>
-<style lang="scss" scoped>
-@import url('@/styles/kb.scss');
 
-.kb-screen {
+<style scoped>
+.full-screen-container {
+  position: absolute;
+  inset: 0;
+  z-index: 1;
+  width: 100%;
+  height: calc(100vh - 90px);
   overflow: hidden;
 }
 
-.kb-content {
-  padding: calc(12px * var(--kb-scale)) calc(20px * var(--kb-scale)) 0;
-}
-
-.home-top-grid,
-.home-bottom-grid {
-  display: grid;
+.full-screen-iframe {
+  display: block;
   width: 100%;
-  margin-top: calc(12px * var(--kb-scale));
-  gap: calc(12px * var(--kb-scale));
-}
-
-.home-top-grid {
-  height: calc(384px * var(--kb-scale));
-  grid-template-rows: repeat(2, minmax(0, 1fr));
-  grid-template-columns: repeat(3, minmax(0, 1fr));
-}
-
-.home-bottom-grid {
-  height: calc(428px * var(--kb-scale));
-  grid-template-columns: repeat(2, minmax(0, 1fr));
+  height: 100%;
+  padding: 0;
+  margin: 0;
+  overflow: auto;
+  border: none;
 }
 </style>

+ 116 - 0
src/views/Home/stat.vue

@@ -0,0 +1,116 @@
+<script lang="ts" setup>
+import { DESIGN_HEIGHT, DESIGN_WIDTH } from '@/utils/kb'
+import hsummary from './kb/hsummary.vue'
+import hdeviceType from './kb/hdeviceType.vue'
+import dayfinish from './kb/dayfinish.vue'
+import hsafe from './kb/hsafe.vue'
+import hdeviceStatus from './kb/hdeviceStatus.vue'
+import horderTrend from './kb/horderTrend.vue'
+
+const company = ref('首页')
+
+const wrapperRef = ref<HTMLDivElement>()
+const scale = ref(1)
+
+let resizeObserver: ResizeObserver | null = null
+let resizeRaf = 0
+
+provide('homeKbScale', scale)
+
+const targetWrapperStyle = computed(() => ({
+  width: `${DESIGN_WIDTH * scale.value}px`,
+  height: `${DESIGN_HEIGHT * scale.value}px`
+}))
+
+const targetAreaStyle = computed(() => {
+  return {
+    '--kb-scale': scale.value,
+    width: `${DESIGN_WIDTH * scale.value}px`,
+    height: `${DESIGN_HEIGHT * scale.value}px`
+  }
+})
+
+function updateScale() {
+  cancelAnimationFrame(resizeRaf)
+
+  resizeRaf = requestAnimationFrame(() => {
+    const wrapper = wrapperRef.value
+    if (!wrapper) return
+
+    const { clientWidth, clientHeight } = wrapper
+    if (!clientWidth || !clientHeight) return
+
+    scale.value = Math.min(clientWidth / DESIGN_WIDTH, clientHeight / DESIGN_HEIGHT)
+    nextTick(() => {
+      window.dispatchEvent(new Event('homekb:resize'))
+    })
+  })
+}
+
+onMounted(() => {
+  nextTick(updateScale)
+  resizeObserver = new ResizeObserver(updateScale)
+  if (wrapperRef.value) {
+    resizeObserver.observe(wrapperRef.value)
+  }
+  window.addEventListener('resize', updateScale)
+})
+
+onUnmounted(() => {
+  resizeObserver?.disconnect()
+  window.removeEventListener('resize', updateScale)
+  cancelAnimationFrame(resizeRaf)
+})
+</script>
+<template>
+  <div ref="wrapperRef" class="bg absolute top-0 left-0 size-full z-10">
+    <div class="mx-a overflow-hidden" :style="targetWrapperStyle">
+      <div class="bg kb-screen" :style="targetAreaStyle">
+        <header class="header">{{ company }}</header>
+        <div class="kb-content">
+          <hsummary class="kb-stage-card kb-stage-card--1" />
+          <div class="home-top-grid">
+            <!-- <hmttr class="kb-stage-card kb-stage-card--2" /> -->
+            <hsafe class="row-span-2 kb-stage-card kb-stage-card--3" />
+            <hdeviceType class="row-span-2 kb-stage-card kb-stage-card--4" />
+            <dayfinish class="row-span-2 kb-stage-card kb-stage-card--5" />
+          </div>
+          <div class="home-bottom-grid">
+            <hdeviceStatus class="kb-stage-card kb-stage-card--6" />
+            <horderTrend class="kb-stage-card kb-stage-card--7" />
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+<style lang="scss" scoped>
+@import url('@/styles/kb.scss');
+
+.kb-screen {
+  overflow: hidden;
+}
+
+.kb-content {
+  padding: calc(12px * var(--kb-scale)) calc(20px * var(--kb-scale)) 0;
+}
+
+.home-top-grid,
+.home-bottom-grid {
+  display: grid;
+  width: 100%;
+  margin-top: calc(12px * var(--kb-scale));
+  gap: calc(12px * var(--kb-scale));
+}
+
+.home-top-grid {
+  height: calc(384px * var(--kb-scale));
+  grid-template-rows: repeat(2, minmax(0, 1fr));
+  grid-template-columns: repeat(3, minmax(0, 1fr));
+}
+
+.home-bottom-grid {
+  height: calc(428px * var(--kb-scale));
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+}
+</style>

+ 39 - 2
src/views/pms/iotrhdailyreport/components/DailyStatistics.vue

@@ -31,6 +31,7 @@ interface Query {
 const props = defineProps<{
   query: Query
   deptName: string
+  defaultView?: '表格' | '看板'
   refreshKey: number
 }>()
 
@@ -267,19 +268,30 @@ const currentTab = ref<'表格' | '看板'>('表格')
 const direction = ref<'left' | 'right'>('right')
 
 const handleSelectTab = (val: '表格' | '看板') => {
+  if (tab.value === val && currentTab.value === val) return
+
   tab.value = val
   direction.value = val === '看板' ? 'right' : 'left'
   nextTick(() => {
     currentTab.value = val
     setTimeout(() => {
-      render()
+      if (val === '看板') scheduleChartRender()
     })
   })
 }
 
+watch(
+  () => props.defaultView,
+  (val) => {
+    handleSelectTab(val || '表格')
+  },
+  { immediate: true }
+)
+
 const chartRef = ref<HTMLDivElement | null>(null)
 let chart: ECharts | null = null
 let chartContainerEl: HTMLDivElement | null = null
+let chartRenderRaf = 0
 
 const xAxisData = ref<string[]>([])
 
@@ -625,7 +637,30 @@ const render = () => {
   chart?.setOption(getChartOption(), { notMerge: true, lazyUpdate: true })
 }
 
+const scheduleChartRender = (attempt = 0) => {
+  cancelAnimationFrame(chartRenderRaf)
+
+  chartRenderRaf = requestAnimationFrame(() => {
+    chartRenderRaf = 0
+
+    if (currentTab.value !== '看板') return
+
+    const chartEl = chartRef.value
+
+    if (!chartEl || !chartEl.clientWidth || !chartEl.clientHeight) {
+      if (attempt < 20) scheduleChartRender(attempt + 1)
+      return
+    }
+
+    render()
+    requestAnimationFrame(() => {
+      chart?.resize()
+    })
+  })
+}
+
 onUnmounted(() => {
+  cancelAnimationFrame(chartRenderRaf)
   window.removeEventListener('resize', resizer)
   chart?.dispose()
   chart = null
@@ -634,7 +669,9 @@ onUnmounted(() => {
 
 const handleQuery = () => {
   getChart().then(() => {
-    render()
+    if (currentTab.value === '看板') {
+      scheduleChartRender()
+    }
   })
   getList()
   getTotal()

+ 50 - 0
src/views/pms/iotrhdailyreport/summary.vue

@@ -9,6 +9,7 @@ defineOptions({
   name: 'IotRhDailyReportSummary'
 })
 
+const route = useRoute()
 const deptId = useUserStore().getUser.deptId
 
 interface Query {
@@ -35,9 +36,46 @@ const createDefaultQuery = (): Query => ({
 
 const query = ref<Query>(createDefaultQuery())
 const activeTab = ref<'日报统计' | '非生产时效'>('日报统计')
+const defaultView = ref<'表格' | '看板'>('表格')
 const deptName = ref('瑞恒兴域')
 const refreshKey = ref(0)
 
+const getRouteCreateTime = () => {
+  const createTime = route.query.createTime
+
+  if (Array.isArray(createTime)) {
+    const values = createTime.filter((item): item is string => typeof item === 'string')
+    return values.length === 2 ? values : undefined
+  }
+
+  if (typeof createTime === 'string') {
+    const values = createTime.split(',').filter(Boolean)
+    return values.length === 2 ? values : undefined
+  }
+
+  return undefined
+}
+
+const getRouteActiveTab = () => {
+  return route.query.activeTab === '非生产时效' ? '非生产时效' : '日报统计'
+}
+
+const getRouteDefaultView = () => {
+  return route.query.view === 'kanban' || route.query.defaultView === '看板' ? '看板' : '表格'
+}
+
+const syncQueryFromRoute = () => {
+  const createTime = getRouteCreateTime()
+
+  if (createTime) {
+    query.value.createTime = createTime
+  }
+
+  activeTab.value = getRouteActiveTab()
+  defaultView.value = getRouteDefaultView()
+  handleQuery()
+}
+
 const handleDeptNodeClick = (node: any) => {
   deptName.value = node.name
   handleQuery()
@@ -53,6 +91,17 @@ const resetQuery = () => {
   deptName.value = '瑞恒兴域'
   handleQuery()
 }
+
+onMounted(() => {
+  syncQueryFromRoute()
+})
+
+watch(
+  () => [route.query.createTime, route.query.activeTab],
+  () => {
+    syncQueryFromRoute()
+  }
+)
 </script>
 
 <template>
@@ -123,6 +172,7 @@ const resetQuery = () => {
       v-if="activeTab === '日报统计'"
       :query="query"
       :dept-name="deptName"
+      :default-view="defaultView"
       :refresh-key="refreshKey" />
     <NonProductionEfficiency
       v-else

+ 369 - 0
src/views/pms/iotrydailyreport/components/equipment-form.vue

@@ -0,0 +1,369 @@
+<script setup lang="ts">
+import { IotRyImproveDailyReportApi } from '@/api/pms/iotryimprovedailyreport'
+import { FormInstance, FormRules } from 'element-plus'
+import { Close } from '@element-plus/icons-vue'
+
+defineOptions({ name: 'IotRyEquipmentReportForm' })
+
+type FormMode = 'create' | 'edit' | 'detail' | 'approval'
+
+interface EquipmentReportForm {
+  id?: number
+  title: string
+  createTime: number | undefined
+  workLocation: string
+  workPurpose: string
+  relocationDays: number | undefined
+  productionStatus: string
+  nextPlan: string
+  personnel: string
+  auditStatus?: 0 | 10 | 20 | 30 | 40
+  opinion?: string
+}
+
+interface Props {
+  visible?: boolean
+  loadList: () => void | Promise<void>
+  reloadAfterCreate?: () => void | Promise<void>
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  visible: false
+})
+
+const emits = defineEmits<{
+  'update:visible': [value: boolean]
+}>()
+
+const defaultForm: EquipmentReportForm = {
+  title: '',
+  createTime: undefined,
+  workLocation: '',
+  workPurpose: '',
+  relocationDays: undefined,
+  productionStatus: '',
+  nextPlan: '',
+  personnel: '',
+  auditStatus: undefined,
+  opinion: ''
+}
+
+const formRef = ref<FormInstance>()
+const formMode = ref<FormMode>('create')
+const form = ref<EquipmentReportForm>({ ...defaultForm })
+const formLoading = ref(false)
+const detailLoading = ref(false)
+const message = useMessage()
+
+const auditStatusMap = {
+  0: { label: '未提交', type: 'info' },
+  10: { label: '审批中', type: 'warning' },
+  20: { label: '审核通过', type: 'success' },
+  30: { label: '审核不通过', type: 'danger' },
+  40: { label: '已取消', type: 'info' }
+} as const
+
+const rules: FormRules = {
+  title: [{ required: true, message: '请输入标题', trigger: ['blur', 'change'] }],
+  createTime: [{ required: true, message: '请选择汇报时间', trigger: ['blur', 'change'] }],
+  workLocation: [{ required: true, message: '请输入工作地点', trigger: ['blur', 'change'] }],
+  workPurpose: [{ required: true, message: '请输入施工目的', trigger: ['blur', 'change'] }],
+  relocationDays: [{ required: true, message: '请输入安全作业天数', trigger: ['blur', 'change'] }],
+  productionStatus: [{ required: true, message: '请输入施工动态', trigger: ['blur', 'change'] }],
+  nextPlan: [{ required: true, message: '请输入下步计划', trigger: ['blur', 'change'] }],
+  personnel: [{ required: true, message: '请输入人员情况', trigger: ['blur', 'change'] }]
+}
+
+const drawerTitle = computed(() => {
+  const titleMap: Record<FormMode, string> = {
+    create: '新建设备汇报',
+    edit: '编辑设备汇报',
+    detail: '查看设备汇报',
+    approval: '审批设备汇报'
+  }
+
+  return titleMap[formMode.value]
+})
+
+const formDisabled = computed(() => formMode.value === 'detail')
+const mainFieldDisabled = computed(
+  () => formMode.value === 'detail' || formMode.value === 'approval'
+)
+const showAuditInfo = computed(() => formMode.value !== 'create')
+
+function getAuditStatus(status?: number) {
+  return auditStatusMap[status as keyof typeof auditStatusMap] || auditStatusMap[0]
+}
+
+function toFormData(row?: any): EquipmentReportForm {
+  if (!row) return { ...defaultForm }
+
+  return {
+    id: row.id,
+    title: row.title,
+    createTime: row.createTime,
+    workLocation: row.workLocation,
+    workPurpose: row.workPurpose,
+    relocationDays: row.relocationDays,
+    productionStatus: row.productionStatus,
+    nextPlan: row.nextPlan,
+    personnel: row.personnel,
+    auditStatus: row.auditStatus,
+    opinion: row.opinion || ''
+  }
+}
+
+async function handleOpenForm(mode: FormMode = 'create', row?: any) {
+  formMode.value = mode
+  form.value = mode === 'create' ? { ...defaultForm } : toFormData(row)
+  emits('update:visible', true)
+
+  if (mode !== 'create' && row?.id) {
+    detailLoading.value = true
+    try {
+      const detail = await IotRyImproveDailyReportApi.getIotRyImproveDailyReport(row.id)
+      form.value = toFormData(detail)
+    } finally {
+      detailLoading.value = false
+    }
+  }
+
+  nextTick(() => {
+    formRef.value?.clearValidate()
+  })
+}
+
+function handleCloseForm() {
+  emits('update:visible', false)
+}
+
+async function submitForm() {
+  if (!formRef.value || formDisabled.value || formMode.value === 'approval') return
+
+  try {
+    formLoading.value = true
+    await formRef.value.validate()
+
+    const data = { ...form.value, createTime: Number(form.value.createTime) }
+
+    if (data.id) {
+      await IotRyImproveDailyReportApi.updateIotRyImproveDailyReport({
+        id: data.id,
+        title: data.title,
+        createTime: data.createTime,
+        workLocation: data.workLocation,
+        workPurpose: data.workPurpose,
+        relocationDays: Number(data.relocationDays),
+        productionStatus: data.productionStatus,
+        personnel: data.personnel,
+        nextPlan: data.nextPlan
+      })
+      message.success('编辑成功')
+      await props.loadList()
+    } else {
+      await IotRyImproveDailyReportApi.createIotRyImproveDailyReport({
+        title: data.title,
+        createTime: data.createTime,
+        workLocation: data.workLocation,
+        workPurpose: data.workPurpose,
+        relocationDays: Number(data.relocationDays),
+        productionStatus: data.productionStatus,
+        personnel: data.personnel,
+        nextPlan: data.nextPlan
+      })
+      message.success('新增成功')
+      await (props.reloadAfterCreate ? props.reloadAfterCreate() : props.loadList())
+    }
+
+    handleCloseForm()
+  } finally {
+    formLoading.value = false
+  }
+}
+
+async function submitApproval(auditStatus: 20 | 30) {
+  if (!form.value.id) return
+
+  try {
+    formLoading.value = true
+    await IotRyImproveDailyReportApi.approvalIotRyImproveDailyReport({
+      id: form.value.id,
+      auditStatus,
+      opinion: form.value.opinion
+    })
+    message.success(auditStatus === 20 ? '审批通过' : '审批不通过')
+    await props.loadList()
+    handleCloseForm()
+  } finally {
+    formLoading.value = false
+  }
+}
+
+defineExpose({ handleOpenForm })
+</script>
+
+<template>
+  <el-drawer
+    :model-value="visible"
+    @update:model-value="emits('update:visible', $event)"
+    header-class="mb-0!"
+    :with-header="false"
+    size="70%"
+    :close-on-press-escape="false"
+    :close-on-click-modal="false">
+    <div class="size-full flex flex-col gap-4">
+      <div class="flex justify-between items-center">
+        <div class="flex items-center gap-4">
+          <div class="text-xl font-bold text-gray-900 leading-tight">{{ drawerTitle }}</div>
+          <template v-if="showAuditInfo">
+            <el-tag :type="getAuditStatus(form.auditStatus).type">
+              {{ getAuditStatus(form.auditStatus).label }}
+            </el-tag>
+          </template>
+        </div>
+        <el-button link @click="handleCloseForm">
+          <el-icon size="24"><Close /></el-icon>
+        </el-button>
+      </div>
+
+      <el-form
+        ref="formRef"
+        v-loading="detailLoading"
+        size="default"
+        label-position="top"
+        :model="form"
+        require-asterisk-position="right"
+        class="flex flex-col"
+        :rules="rules"
+        :disabled="formDisabled">
+        <div class="grid grid-cols-2 gap-x-8">
+          <el-form-item class="col-span-2" label="标题" prop="title">
+            <el-input
+              v-model="form.title"
+              clearable
+              placeholder="请输入标题"
+              :disabled="mainFieldDisabled" />
+          </el-form-item>
+
+          <el-form-item label="汇报时间" prop="createTime">
+            <el-date-picker
+              v-model="form.createTime"
+              type="date"
+              clearable
+              class="w-full!"
+              placeholder="请选择汇报时间"
+              value-format="x"
+              :disabled="mainFieldDisabled" />
+          </el-form-item>
+
+          <el-form-item label="工作地点" prop="workLocation">
+            <el-input
+              v-model="form.workLocation"
+              clearable
+              placeholder="请输入工作地点"
+              :disabled="mainFieldDisabled" />
+          </el-form-item>
+
+          <el-form-item label="施工目的" prop="workPurpose">
+            <el-input
+              v-model="form.workPurpose"
+              clearable
+              placeholder="请输入施工目的"
+              :disabled="mainFieldDisabled" />
+          </el-form-item>
+
+          <el-form-item label="安全作业天数" prop="relocationDays">
+            <el-input-number
+              v-model="form.relocationDays"
+              :min="0"
+              :controls="false"
+              align="left"
+              class="w-full!"
+              placeholder="请输入安全作业天数"
+              :disabled="mainFieldDisabled">
+              <template #suffix>天</template>
+            </el-input-number>
+          </el-form-item>
+
+          <el-form-item label="人员情况" prop="personnel">
+            <el-input
+              v-model="form.personnel"
+              clearable
+              placeholder="请输入人员情况"
+              :disabled="mainFieldDisabled" />
+          </el-form-item>
+
+          <el-form-item class="col-span-2" label="施工动态" prop="productionStatus">
+            <el-input
+              v-model="form.productionStatus"
+              type="textarea"
+              :autosize="{ minRows: 8 }"
+              resize="none"
+              show-word-limit
+              :maxlength="2000"
+              placeholder="请输入施工动态,每行一条"
+              :disabled="mainFieldDisabled" />
+          </el-form-item>
+
+          <el-form-item class="col-span-2" label="下步计划" prop="nextPlan">
+            <el-input
+              v-model="form.nextPlan"
+              type="textarea"
+              :autosize="{ minRows: 2 }"
+              resize="none"
+              show-word-limit
+              :maxlength="1000"
+              placeholder="请输入下步计划"
+              :disabled="mainFieldDisabled" />
+          </el-form-item>
+
+          <el-form-item v-if="showAuditInfo" class="col-span-2" label="审批意见" prop="opinion">
+            <el-input
+              v-model="form.opinion"
+              type="textarea"
+              :autosize="{ minRows: 3 }"
+              resize="none"
+              show-word-limit
+              :maxlength="1000"
+              :placeholder="formMode === 'approval' ? '请输入审批意见' : '暂无审批意见'"
+              :disabled="formMode !== 'approval'" />
+          </el-form-item>
+        </div>
+      </el-form>
+    </div>
+
+    <template #footer>
+      <el-button
+        v-if="formMode === 'create' || formMode === 'edit'"
+        size="default"
+        type="primary"
+        @click="submitForm"
+        :loading="formLoading">
+        确 定
+      </el-button>
+      <el-button
+        v-if="formMode === 'approval'"
+        size="default"
+        type="success"
+        @click="submitApproval(20)"
+        :loading="formLoading">
+        审批通过
+      </el-button>
+      <el-button
+        v-if="formMode === 'approval'"
+        size="default"
+        type="danger"
+        @click="submitApproval(30)"
+        :loading="formLoading">
+        审批不通过
+      </el-button>
+      <el-button size="default" @click="handleCloseForm">取 消</el-button>
+    </template>
+  </el-drawer>
+</template>
+
+<style scoped>
+:deep(.el-form-item__label) {
+  font-weight: 500;
+}
+</style>

+ 236 - 0
src/views/pms/iotrydailyreport/equipment.vue

@@ -0,0 +1,236 @@
+<script lang="ts" setup>
+import { IotRyImproveDailyReportApi } from '@/api/pms/iotryimprovedailyreport'
+import { useTableComponents } from '@/components/ZmTable/useTableComponents'
+import dayjs from 'dayjs'
+import EquipmentForm from './components/equipment-form.vue'
+import { useUserStore } from '../../../store/modules/user.js'
+
+defineOptions({ name: 'IotRyEquipmentDailyReport' })
+
+interface ReportRow {
+  id: number
+  title: string
+  createTime: number
+  workLocation: string
+  workPurpose: string
+  relocationDays: number
+  productionStatus: string
+  nextPlan: string
+  personnel: string
+  auditStatus: 0 | 10 | 20 | 30 | 40
+}
+
+interface Query {
+  deptId?: number
+  pageNo: number
+  pageSize: number
+  createTime?: string[]
+}
+
+const deptId = useUserStore().getUser.deptId
+const loading = ref(false)
+const { ZmTable, ZmTableColumn } = useTableComponents<ReportRow>()
+const formRef = ref()
+const formVisible = ref(false)
+const total = ref(0)
+
+const query = ref<Query>({
+  deptId: undefined,
+  pageNo: 1,
+  pageSize: 10,
+  createTime: undefined
+})
+
+const reportRows = ref<ReportRow[]>([])
+
+async function loadList() {
+  loading.value = true
+  try {
+    const data = await IotRyImproveDailyReportApi.getIotRyImproveDailyReportPage(query.value)
+    reportRows.value = data.list || []
+    total.value = data.total || 0
+  } finally {
+    loading.value = false
+  }
+}
+
+function handleQuery() {
+  query.value.pageNo = 1
+  loadList()
+}
+
+function resetQuery() {
+  query.value = {
+    pageNo: 1,
+    pageSize: 10,
+    createTime: undefined
+  }
+  loadList()
+}
+
+function handleSizeChange(val: number) {
+  query.value.pageSize = val
+  handleQuery()
+}
+
+function handleCurrentChange(val: number) {
+  query.value.pageNo = val
+  loadList()
+}
+
+const auditStatusMap = {
+  0: { label: '未提交', type: 'info' },
+  10: { label: '审批中', type: 'warning' },
+  20: { label: '审核通过', type: 'success' },
+  30: { label: '审核不通过', type: 'danger' },
+  40: { label: '已取消', type: 'info' }
+} as const
+
+function getAuditStatus(status?: number) {
+  return auditStatusMap[status as keyof typeof auditStatusMap] || auditStatusMap[0]
+}
+
+function handleOpenForm(type: 'create' | 'edit' | 'detail' | 'approval', row?: ReportRow) {
+  formRef.value?.handleOpenForm(type, row)
+}
+
+function reloadAfterCreate() {
+  query.value.pageNo = 1
+  return loadList()
+}
+
+watch(() => query.value.deptId, handleQuery)
+
+onMounted(() => {
+  loadList()
+})
+</script>
+
+<template>
+  <div
+    class="grid grid-cols-[auto_1fr] grid-rows-[48px_1fr] gap-x-4 gap-y-3 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]">
+    <DeptTreeSelect
+      :top-id="158"
+      :deptId="deptId"
+      v-model="query.deptId"
+      :show-title="false"
+      class="row-span-2" />
+
+    <el-form
+      size="default"
+      class="bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-6 gap-8 flex items-center justify-between">
+      <div class="flex items-center gap-8">
+        <el-form-item label="汇报时间">
+          <el-date-picker
+            v-model="query.createTime"
+            value-format="YYYY-MM-DD HH:mm:ss"
+            type="daterange"
+            start-placeholder="开始日期"
+            end-placeholder="结束日期"
+            :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+            class="!w-260px" />
+        </el-form-item>
+      </div>
+      <el-form-item>
+        <el-button
+          type="primary"
+          @click="handleOpenForm('create')"
+          v-hasPermi="['pms:iot-ry-improve-daily-report:create']">
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </el-button>
+        <el-button type="primary" @click="handleQuery">
+          <Icon icon="ep:search" class="mr-5px" /> 搜索
+        </el-button>
+        <el-button @click="resetQuery"> <Icon icon="ep:refresh" class="mr-5px" /> 重置 </el-button>
+      </el-form-item>
+    </el-form>
+
+    <div class="bg-white dark:bg-[#1d1e1f] shadow rounded-lg flex flex-col p-4 min-h-0">
+      <div class="flex-1 relative">
+        <el-auto-resizer class="absolute">
+          <template #default="{ width, height }">
+            <zm-table
+              :data="reportRows"
+              :loading="loading"
+              :width="width"
+              :max-height="height"
+              :height="height"
+              empty-text="暂无设备搬迁整改项目汇报"
+              show-border>
+              <zm-table-column type="index" label="序号" width="70" fixed="left" />
+              <zm-table-column label="标题" prop="title" fixed="left" />
+              <zm-table-column label="施工队伍" prop="deptName" />
+              <zm-table-column label="汇报时间" prop="createTime">
+                <template #default="{ row }">
+                  {{ dayjs(row.createTime).format('YYYY.MM.DD') }}
+                </template>
+              </zm-table-column>
+              <zm-table-column label="工作地点" prop="workLocation" />
+              <zm-table-column label="施工目的" prop="workPurpose" />
+              <zm-table-column
+                label="安全作业天数"
+                prop="relocationDays"
+                :real-value="(row) => row.relocationDays + '天'"
+                cover-formatter />
+              <zm-table-column label="施工动态" prop="productionStatus" />
+              <zm-table-column label="下步计划" prop="nextPlan" />
+              <zm-table-column label="人员情况" prop="personnel" />
+              <zm-table-column label="审批状态" prop="auditStatus" width="120">
+                <template #default="{ row }">
+                  <el-tag :type="getAuditStatus(row.auditStatus).type">
+                    {{ getAuditStatus(row.auditStatus).label }}
+                  </el-tag>
+                </template>
+              </zm-table-column>
+              <zm-table-column label="操作" width="160" fixed="right" action>
+                <template #default="{ row }">
+                  <el-button link type="primary" @click="handleOpenForm('detail', row)">
+                    查看
+                  </el-button>
+                  <el-button
+                    v-if="row.auditStatus !== 20"
+                    link
+                    type="primary"
+                    @click="handleOpenForm('edit', row)"
+                    v-hasPermi="['pms:iot-ry-improve-daily-report:update']">
+                    编辑
+                  </el-button>
+                  <el-button
+                    v-if="row.auditStatus === 10"
+                    link
+                    type="primary"
+                    @click="handleOpenForm('approval', row)"
+                    v-hasPermi="['pms:iot-ry-improve-daily-report:approval']">
+                    审批
+                  </el-button>
+                </template>
+              </zm-table-column>
+            </zm-table>
+          </template>
+        </el-auto-resizer>
+      </div>
+      <div class="mt-3 flex justify-end">
+        <el-pagination
+          v-model:current-page="query.pageNo"
+          v-model:page-size="query.pageSize"
+          :total="total"
+          :page-sizes="[10, 20, 30, 50, 100]"
+          layout="total, sizes, prev, pager, next, jumper"
+          @size-change="handleSizeChange"
+          @current-change="handleCurrentChange" />
+      </div>
+    </div>
+  </div>
+
+  <EquipmentForm
+    ref="formRef"
+    v-model:visible="formVisible"
+    :load-list="loadList"
+    :reload-after-create="reloadAfterCreate" />
+</template>
+
+<style scoped>
+:deep(.el-form-item) {
+  margin-bottom: 0;
+}
+</style>

+ 16 - 8
src/views/pms/operation-meeting/components/operation-meeting-content.vue

@@ -132,12 +132,7 @@ const nextPlanBaseDetailCardFields: DetailCardField[] = [
   { label: '重点工作事项', prop: 'priorityTasks' }
 ]
 
-const meetingTableBlueColumns = new Set<keyof DetailItem>([
-  'projectName',
-  'keyWorkCompletion',
-  'problemsAnalysis',
-  'qhse'
-])
+const meetingTableBlueColumns = new Set<keyof DetailItem>(['keyWorkCompletion', 'problemsAnalysis'])
 
 const isSummaryMode = computed(() => props.mode === 'summary')
 const canEditDetails = computed(() => !isSummaryMode.value && props.type !== 'view')
@@ -705,7 +700,7 @@ const getMeetingTableCellStyle: any = ({
     return { color: '#1b71f6' }
   }
 
-  return undefined
+  return property ? { color: '#000' } : undefined
 }
 
 const getMeetingTableRowStyle = (): CSSProperties => ({
@@ -1526,7 +1521,7 @@ const getMeetingTableCellClassName = ({ column }: MeetingTableCellStyleProps) =>
   --zm-table-header-font-size: 18px;
   --zm-table-header-font-weight: 800;
   --zm-table-row-font-weight: 800;
-  --zm-table-header-text-color: #333;
+  --zm-table-header-text-color: #fff;
   --zm-table-border-color: #cbd5e1;
   --zm-table-radius: 0;
   --zm-table-header-border-color: #c2ccda;
@@ -1557,6 +1552,19 @@ const getMeetingTableCellClassName = ({ column }: MeetingTableCellStyleProps) =>
     background: #fff;
   }
 
+  :deep(.el-table__header .el-table__cell),
+  :deep(.el-table__fixed-header-wrapper .el-table__cell) {
+    color: #fff;
+    background-color: #0447ad !important;
+  }
+
+  :deep(.el-table__header .cell),
+  :deep(.el-table__fixed-header-wrapper .cell),
+  :deep(.el-table__header .header-wrapper),
+  :deep(.el-table__fixed-header-wrapper .header-wrapper) {
+    color: #fff;
+  }
+
   :deep(.header-wrapper) {
     height: 20px;
     justify-content: center !important;

+ 109 - 13
src/views/pms/stat/rhkb.vue

@@ -5,16 +5,25 @@ import rhsummary from './rhkb/rhsummary.vue'
 import deviceType from './rhkb/deviceType.vue'
 import deviceStatus from './rhkb/deviceStatus.vue'
 import operation from './rhkb/operation.vue'
-import orderTrend from './rhkb/orderTrend.vue'
 import todayGas from './rhkb/todayGas.vue'
 import historyGas from './rhkb/historyGas.vue'
 import deviceList from './rhkb/deviceList.vue'
+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'
 
 defineOptions({
   name: 'IotRhStatt'
 })
 
 const company = ref('瑞恒')
+const activePage = ref<'home' | 'device'>('home')
+
+const pageTabs = [
+  { label: '运行概况', value: 'home' },
+  { label: '生产日报', value: 'device' }
+] as const
 
 const wrapperRef = ref<HTMLDivElement>()
 const scale = ref(1)
@@ -63,6 +72,12 @@ onMounted(() => {
   window.addEventListener('resize', updateScale)
 })
 
+watch(activePage, () => {
+  nextTick(() => {
+    window.dispatchEvent(new Event('rhkb:resize'))
+  })
+})
+
 onUnmounted(() => {
   resizeObserver?.disconnect()
   window.removeEventListener('resize', updateScale)
@@ -76,16 +91,38 @@ onUnmounted(() => {
       <div class="bg kb-screen" id="rhkb" :style="targetAreaStyle">
         <header class="header">{{ company }}</header>
         <div class="kb-content">
-          <rhsummary class="kb-stage-card kb-stage-card--1" />
-          <div class="kb-chart-grid">
-            <deviceStatus class="kb-stage-card kb-stage-card--2" />
-            <deviceType class="kb-stage-card kb-stage-card--3" />
-            <operation class="kb-stage-card kb-stage-card--4" />
-            <orderTrend class="kb-stage-card kb-stage-card--5" />
-            <todayGas class="kb-stage-card kb-stage-card--6" />
-            <historyGas class="kb-stage-card kb-stage-card--7" />
+          <div class="page-tabs">
+            <button
+              v-for="tab in pageTabs"
+              :key="tab.value"
+              type="button"
+              class="page-tab"
+              :class="{ 'is-active': activePage === tab.value }"
+              @click="activePage = tab.value">
+              {{ tab.label }}
+            </button>
+          </div>
+
+          <div v-if="activePage === 'home'" class="kb-home-page">
+            <rhsummary class="kb-stage-card kb-stage-card--1" />
+            <div class="kb-chart-grid">
+              <deviceStatus class="kb-stage-card kb-stage-card--1" />
+              <deviceType class="kb-stage-card kb-stage-card--2" />
+              <equipmentRate class="kb-stage-card kb-stage-card--3" />
+              <rhsafeday class="kb-stage-card kb-stage-card--4" />
+              <todayGas class="kb-stage-card kb-stage-card--5" />
+              <historyGas 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" />
+            </div>
+          </div>
+
+          <div v-else class="kb-device-page">
+            <deviceList
+              class="kb-stage-card kb-stage-card--8 kb-stage-card--list"
+              page-mode="full" />
           </div>
-          <deviceList class="kb-stage-card kb-stage-card--8 kb-stage-card--list" />
         </div>
       </div>
     </div>
@@ -100,16 +137,75 @@ onUnmounted(() => {
 }
 
 .kb-content {
-  padding: calc(12px * var(--kb-scale)) calc(20px * var(--kb-scale)) 0;
+  position: relative;
+  height: calc(100% - 52px * var(--kb-scale));
+  padding: calc(44px * var(--kb-scale)) calc(20px * var(--kb-scale)) calc(20px * var(--kb-scale));
+}
+
+.page-tabs {
+  position: absolute;
+  top: calc(10px * var(--kb-scale));
+  left: calc(20px * var(--kb-scale));
+  z-index: 3;
+  display: flex;
+  width: fit-content;
+  gap: calc(12px * var(--kb-scale));
+}
+
+.page-tab {
+  height: calc(28px * var(--kb-scale));
+  min-width: calc(82px * var(--kb-scale));
+  padding: 0 calc(14px * var(--kb-scale));
+  font-family: YouSheBiaoTiHei, sans-serif;
+  font-size: calc(15px * var(--kb-scale));
+  line-height: calc(28px * var(--kb-scale));
+  color: #f5f9ff;
+  cursor: pointer;
+  background: linear-gradient(180deg, #83bcff 0%, #2f7ee9 58%, #1762d6 100%);
+  border: 1px solid rgb(255 255 255 / 55%);
+  border-radius: calc(5px * var(--kb-scale));
+  box-shadow:
+    inset 0 1px 0 rgb(255 255 255 / 58%),
+    0 calc(4px * var(--kb-scale)) calc(8px * var(--kb-scale)) rgb(30 89 179 / 22%);
+  transition:
+    transform 0.2s ease,
+    filter 0.2s ease,
+    box-shadow 0.2s ease;
+}
+
+.page-tab:hover,
+.page-tab.is-active {
+  filter: brightness(1.08);
+  transform: translateY(calc(-1px * var(--kb-scale)));
+  box-shadow:
+    inset 0 1px 0 rgb(255 255 255 / 72%),
+    0 calc(5px * var(--kb-scale)) calc(10px * var(--kb-scale)) rgb(30 89 179 / 28%);
+}
+
+.page-tab.is-active {
+  background: linear-gradient(180deg, #4d9cff 0%, #1f6ee7 56%, #0e4fc4 100%);
+}
+
+.kb-home-page {
+  display: flex;
+  height: 100%;
+  min-height: 0;
+  flex-direction: column;
 }
 
 .kb-chart-grid {
   display: grid;
   width: 100%;
-  height: calc(592px * var(--kb-scale));
+  min-height: 0;
+  flex: 1;
   margin-top: calc(12px * var(--kb-scale));
   gap: calc(12px * var(--kb-scale));
-  grid-template-rows: repeat(2, minmax(0, 1fr));
+  grid-template-rows: repeat(3, minmax(0, 1fr));
   grid-template-columns: repeat(3, minmax(0, 1fr));
 }
+
+.kb-device-page {
+  height: 100%;
+  min-height: 0;
+}
 </style>

+ 190 - 0
src/views/pms/stat/rhkb/assetValue.vue

@@ -0,0 +1,190 @@
+<script lang="ts" setup>
+import * as echarts from 'echarts'
+import { ANIMATION, CHART_RENDERER, createTooltip, FONT_FAMILY, THEME } from '@/utils/kb'
+
+const chartRef = ref<HTMLDivElement>()
+let chart: echarts.ECharts | null = null
+
+const assetValueData = [
+  { name: '原值', value: 85000.77 },
+  { name: '本期计提', value: 512.61 },
+  { name: '累计折旧', value: 57722.06 },
+  { name: '净值', value: 27152.6 }
+]
+
+function getChartOption(): echarts.EChartsOption {
+  return {
+    ...ANIMATION,
+    color: [THEME.color.blue.line],
+    grid: {
+      ...THEME.grid,
+      top: 32,
+      right: 18,
+      bottom: 12,
+      left: 18
+    },
+    tooltip: createTooltip({
+      trigger: 'axis',
+      axisPointer: {
+        type: 'shadow',
+        shadowStyle: {
+          color: THEME.split
+        }
+      },
+      valueFormatter(value: number) {
+        return `${Number(value || 0).toLocaleString()} 万元`
+      }
+    }),
+    xAxis: {
+      type: 'category',
+      data: assetValueData.map((item) => item.name),
+      axisLine: {
+        show: false
+      },
+      axisTick: {
+        show: false
+      },
+      axisLabel: {
+        color: THEME.text.regular,
+        fontSize: 14,
+        fontWeight: 500,
+        fontFamily: FONT_FAMILY
+      }
+    },
+    yAxis: {
+      type: 'value',
+      name: '资产价值(万元)',
+      min: 0,
+      max: 90000,
+      splitNumber: 4,
+      nameTextStyle: {
+        color: THEME.text.regular,
+        fontSize: 13,
+        fontFamily: FONT_FAMILY
+      },
+      axisLine: {
+        show: false
+      },
+      axisTick: {
+        show: false
+      },
+      axisLabel: {
+        color: THEME.text.regular,
+        fontSize: 12,
+        fontFamily: FONT_FAMILY,
+        formatter(value: number) {
+          return value.toLocaleString()
+        }
+      },
+      splitLine: {
+        lineStyle: {
+          color: THEME.split,
+          type: 'dashed'
+        }
+      }
+    },
+    series: [
+      {
+        name: '瑞恒',
+        type: 'bar',
+        data: assetValueData.map((item) => item.value),
+        barWidth: 34,
+        showBackground: true,
+        backgroundStyle: {
+          color: THEME.split,
+          borderRadius: 999
+        },
+        itemStyle: {
+          borderRadius: [12, 12, 0, 0],
+          shadowBlur: 12,
+          shadowColor: THEME.color.blue.bg,
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+            { offset: 0, color: THEME.color.blue.light },
+            { offset: 0.5, color: THEME.color.blue.mid },
+            { offset: 1, color: THEME.color.blue.line }
+          ])
+        },
+        label: {
+          show: true,
+          position: 'top',
+          distance: 6,
+          color: THEME.color.blue.strong,
+          fontSize: 13,
+          fontWeight: 700,
+          fontFamily: FONT_FAMILY,
+          formatter(params: any) {
+            return `${assetValueData[params.dataIndex]?.value ?? params.value}`
+          }
+        }
+      }
+    ]
+  }
+}
+
+function initChart() {
+  if (!chartRef.value) return
+
+  if (chart) {
+    chart.dispose()
+  }
+
+  chart = echarts.init(chartRef.value, undefined, {
+    renderer: CHART_RENDERER
+  })
+  renderChart()
+}
+
+function renderChart() {
+  chart?.setOption(getChartOption(), true)
+}
+
+function resizeChart() {
+  chart?.resize()
+}
+
+function destroyChart() {
+  chart?.dispose()
+  chart = null
+}
+
+onMounted(() => {
+  initChart()
+  window.addEventListener('resize', resizeChart)
+  window.addEventListener('rhkb:resize', resizeChart)
+})
+
+onUnmounted(() => {
+  window.removeEventListener('resize', resizeChart)
+  window.removeEventListener('rhkb:resize', resizeChart)
+  destroyChart()
+})
+</script>
+
+<template>
+  <div class="panel asset-value-panel">
+    <div class="panel-title asset-value-title">
+      <div class="icon-decorator">
+        <span></span>
+        <span></span>
+      </div>
+      资产价值
+    </div>
+    <div ref="chartRef" class="asset-value-chart"></div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+@import url('@/styles/kb.scss');
+
+.asset-value-panel {
+  display: flex;
+  min-height: 0;
+  flex-direction: column;
+}
+
+.asset-value-chart {
+  width: 100%;
+  min-height: 0;
+  flex: 1;
+}
+</style>

+ 38 - 3
src/views/pms/stat/rhkb/deviceList.vue

@@ -28,6 +28,15 @@ const DEFAULT_TIME_RANGE = rangeShortcuts[2]
   .value()
   .map((item) => dayjs(item).format('YYYY-MM-DD HH:mm:ss'))
 
+const props = withDefaults(
+  defineProps<{
+    pageMode?: 'compact' | 'full'
+  }>(),
+  {
+    pageMode: 'compact'
+  }
+)
+
 const createTime = ref<string[]>(DEFAULT_TIME_RANGE)
 const loading = ref(false)
 const list = ref<RhDeviceListRow[]>([])
@@ -38,7 +47,9 @@ const teamList = ref<RhTeamRateRow[]>([])
 const kbScale = inject<Ref<number>>('rhKbScale', ref(1))
 
 const tableData = computed(() => list.value)
-const tableHeight = computed(() => Math.round(TABLE_HEIGHT * kbScale.value))
+const tableHeight = computed<number | string>(() =>
+  props.pageMode === 'full' ? '100%' : Math.round(TABLE_HEIGHT * kbScale.value)
+)
 
 function formatRate(value?: number | null) {
   return `${(Number(value ?? 0) * 100).toFixed(2)}%`
@@ -97,7 +108,9 @@ onMounted(() => {
 </script>
 
 <template>
-  <div class="panel device-list-panel w-full flex flex-col">
+  <div
+    class="panel device-list-panel w-full min-h-0 flex flex-col"
+    :class="{ 'device-list-panel--full': props.pageMode === 'full' }">
     <div class="panel-title device-list-panel__title flex items-center justify-between">
       <div class="kb-panel-title-text flex items-center">
         <div class="icon-decorator">
@@ -121,11 +134,12 @@ onMounted(() => {
       </div>
     </div>
     <!-- v-loading="loading" -->
-    <div class="device-list-panel__body flex-1 min-h-0">
+    <div class="device-list-panel__body flex flex-col flex-1 min-h-0">
       <el-table
         :data="tableData"
         :height="tableHeight"
         class="device-list-table"
+        :class="{ 'device-list-table--full': props.pageMode === 'full' }"
         @row-click="handleRowClick">
         <el-table-column prop="projectDeptName" label="项目部" min-width="220" align="center" />
         <el-table-column prop="teamCount" label="队伍数量" min-width="120" align="center" />
@@ -183,6 +197,27 @@ onMounted(() => {
 
 <style lang="scss" scoped>
 @import url('@/styles/kb.scss');
+
+.device-list-panel.device-list-panel--full {
+  height: 100%;
+  min-height: 0;
+  margin-top: 0;
+}
+
+.device-list-table--full {
+  :deep(.el-scrollbar__view) {
+    display: block;
+    height: 100%;
+  }
+
+  :deep(.el-table__body) {
+    height: 100%;
+  }
+
+  :deep(.el-table__body tbody) {
+    height: 100%;
+  }
+}
 </style>
 
 <style>

+ 41 - 13
src/views/pms/stat/rhkb/deviceStatus.vue

@@ -12,41 +12,69 @@ function getChartOption(data: ChartItem[]): echarts.EChartsOption {
   return {
     ...ANIMATION,
     grid: THEME.grid,
-    tooltip: createTooltip({ trigger: 'item' }),
+    tooltip: createTooltip({
+      trigger: 'item',
+      formatter(params: any) {
+        return `
+          <div style="font-weight: 600; margin-bottom: 6px;">${params.seriesName}</div>
+          <div style="display: flex; align-items: center; gap: 8px;">
+            <span style="display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: ${params.color};"></span>
+            <span>${params.name}</span>
+            <span style="font-weight: 700; color: ${THEME.text.strong};">${params.value}</span>
+            <span style="color: ${THEME.text.strong};">${Number(params.percent ?? 0).toFixed(0)}%</span>
+          </div>
+        `
+      }
+    }),
     legend: createLegend(
-      { bottom: 10, itemWidth: 12, itemHeight: 12 },
+      { bottom: 4, itemWidth: 8, itemHeight: 8 },
       data.map((item) => item.name)
     ),
     series: [
       {
         name: '设备状态',
         type: 'pie',
-        center: ['50%', '44%'],
-        radius: ['50%', '70%'],
+        center: ['50%', '45%'],
+        radius: ['44%', '63%'],
         minAngle: 5,
         label: {
-          show: false,
-          position: 'center',
+          show: true,
+          position: 'outside',
+          alignTo: 'edge',
+          edgeDistance: 12,
+          distanceToLabelLine: 5,
           formatter(params: any) {
-            return `{name|${params.name}}\n{value|${params.value}}`
+            return `{name|${params.name}} {percent|${Number(params.percent ?? 0).toFixed(0)}%}`
           },
           rich: {
             name: {
-              color: THEME.text.regular,
-              fontSize: 14,
+              color: THEME.text.primary,
+              fontSize: 11,
               fontWeight: 500,
-              lineHeight: 24,
+              lineHeight: 16,
               fontFamily: FONT_FAMILY
             },
-            value: {
+            percent: {
               color: THEME.text.strong,
-              fontSize: 28,
+              fontSize: 12,
               fontWeight: 700,
-              lineHeight: 36,
+              lineHeight: 16,
               fontFamily: FONT_FAMILY
             }
           }
         },
+        labelLine: {
+          show: true,
+          length: 10,
+          length2: 6,
+          smooth: 0.15,
+          lineStyle: {
+            width: 1
+          }
+        },
+        labelLayout: {
+          hideOverlap: true
+        },
         emphasis: {
           label: {
             show: true

+ 1 - 1
src/views/pms/stat/rhkb/deviceType.vue

@@ -168,7 +168,7 @@ onUnmounted(() => {
         <span></span>
         <span></span>
       </div>
-      设备类别top
+      设备类别/状态
     </div>
     <div ref="chartRef" class="flex-1 min-h-0"></div>
   </div>

+ 244 - 0
src/views/pms/stat/rhkb/equipment-rate.vue

@@ -0,0 +1,244 @@
+<script lang="ts" setup>
+import * as echarts from 'echarts'
+import dayjs from 'dayjs'
+import {
+  ANIMATION,
+  ChartData,
+  CHART_RENDERER,
+  createTooltip,
+  FONT_FAMILY,
+  formatDateLabel,
+  THEME
+} from '@/utils/kb'
+import { IotStatApi } from '@/api/pms/stat'
+
+const chartData = ref<ChartData>({
+  xAxis: [],
+  series: []
+})
+
+const chartRef = ref<HTMLDivElement>()
+let chart: echarts.ECharts | null = null
+const router = useRouter()
+let chartClickBound = false
+
+function formatRate(value: number) {
+  return `${Number(value || 0).toFixed(2)}%`
+}
+
+function getChartOption(data: ChartData): echarts.EChartsOption {
+  const names = data.xAxis || []
+  const values = (data.series?.[0]?.data || []).map((value) => Number(value || 0) * 100)
+
+  return {
+    ...ANIMATION,
+    grid: { ...THEME.grid, top: 40, right: 28, left: 32 },
+    tooltip: createTooltip({
+      trigger: 'axis',
+      axisPointer: {
+        type: 'line'
+      },
+      valueFormatter(value: number) {
+        return formatRate(value)
+      }
+    }),
+    xAxis: {
+      type: 'category',
+      data: names,
+      axisLine: {
+        show: false
+      },
+      axisTick: {
+        show: false
+      },
+      axisLabel: {
+        color: THEME.text.regular,
+        fontSize: 14,
+        fontWeight: 500,
+        fontFamily: FONT_FAMILY,
+        formatter(value: string) {
+          return formatDateLabel(value)
+        }
+      }
+    },
+    yAxis: {
+      type: 'value',
+      name: '设备利用率(%)',
+      min: 0,
+      max: 100,
+      splitNumber: 4,
+      nameTextStyle: {
+        color: THEME.text.regular,
+        fontSize: 13,
+        fontFamily: FONT_FAMILY
+      },
+      axisLine: {
+        show: false
+      },
+      axisTick: {
+        show: false
+      },
+      axisLabel: {
+        color: THEME.text.regular,
+        fontSize: 12,
+        fontFamily: FONT_FAMILY,
+        formatter(value: number) {
+          return `${value}`
+        }
+      },
+      splitLine: {
+        lineStyle: {
+          color: THEME.split,
+          type: 'dashed'
+        }
+      }
+    },
+    series: [
+      {
+        name: '设备利用率',
+        type: 'line',
+        smooth: true,
+        data: values,
+        symbol: 'circle',
+        symbolSize: 8,
+        showSymbol: true,
+        lineStyle: {
+          width: 2,
+          color: THEME.color.green.line
+        },
+        itemStyle: {
+          color: THEME.color.green.line
+        },
+        areaStyle: {
+          color: THEME.color.green.bg
+        },
+        label: {
+          show: true,
+          position: 'top',
+          distance: 10,
+          color: THEME.color.green.strong,
+          fontSize: 14,
+          fontWeight: 700,
+          fontFamily: FONT_FAMILY,
+          formatter(params: any) {
+            return formatRate(Number(params.value))
+          }
+        },
+        emphasis: {
+          focus: 'series',
+          scale: true
+        }
+      }
+    ]
+  }
+}
+
+function initChart() {
+  if (!chartRef.value) return
+  if (chart) {
+    chart.dispose()
+  }
+  chart = echarts.init(chartRef.value, undefined, {
+    renderer: CHART_RENDERER
+  })
+  chart.getZr().on('click', handleChartClick)
+  chartClickBound = true
+  renderChart()
+}
+
+function getChartDayRange() {
+  const xAxis = chartData.value.xAxis || []
+  const startDate = dayjs(xAxis[0])
+  const endDate = dayjs(xAxis[xAxis.length - 1])
+  if (!startDate.isValid() || !endDate.isValid()) return null
+
+  return [
+    startDate.startOf('day').format('YYYY-MM-DD HH:mm:ss'),
+    endDate.endOf('day').format('YYYY-MM-DD HH:mm:ss')
+  ]
+}
+
+function handleChartClick() {
+  const createTime = getChartDayRange()
+  if (!createTime) return
+
+  router.push({
+    name: 'IotRhDailyReportSummary',
+    query: {
+      activeTab: '日报统计',
+      view: 'kanban',
+      createTime
+    }
+  })
+}
+
+function renderChart() {
+  chart?.setOption(getChartOption(chartData.value), true)
+}
+
+function resizeChart() {
+  chart?.resize()
+}
+
+function destroyChart() {
+  if (chart) {
+    if (chartClickBound) {
+      chart.getZr().off('click', handleChartClick)
+      chartClickBound = false
+    }
+    chart.dispose()
+    chart = null
+  }
+}
+
+async function loadChart() {
+  try {
+    const res = await IotStatApi.getRhSevenDayUtilization()
+    chartData.value = {
+      xAxis: res.xAxis || [],
+      series: (res.series || []).map((item) => ({
+        name: item.name,
+        data: item.data || []
+      }))
+    }
+    renderChart()
+  } catch (error) {
+    console.error('获取设备利用率失败:', error)
+    chartData.value = {
+      xAxis: [],
+      series: []
+    }
+    renderChart()
+  }
+}
+
+onMounted(() => {
+  initChart()
+  loadChart()
+  window.addEventListener('resize', resizeChart)
+  window.addEventListener('rhkb:resize', resizeChart)
+})
+
+onUnmounted(() => {
+  window.removeEventListener('resize', resizeChart)
+  window.removeEventListener('rhkb:resize', resizeChart)
+  destroyChart()
+})
+</script>
+
+<template>
+  <div class="panel flex flex-col">
+    <div class="panel-title">
+      <div class="icon-decorator">
+        <span></span>
+        <span></span>
+      </div>
+      设备利用率
+    </div>
+    <div ref="chartRef" class="flex-1 min-h-0"></div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+@import url('@/styles/kb.scss');
+</style>

+ 35 - 1
src/views/pms/stat/rhkb/historyGas.vue

@@ -1,5 +1,6 @@
 <script lang="ts" setup>
 import * as echarts from 'echarts'
+import dayjs from 'dayjs'
 import {
   ANIMATION,
   ChartItem,
@@ -14,8 +15,10 @@ import { IotStatApi } from '@/api/pms/stat'
 
 const chartData = ref<ChartItem[]>([])
 
+const router = useRouter()
 const chartRef = ref<HTMLDivElement>()
 let chart: echarts.ECharts | null = null
+let chartClickBound = false
 
 function getChartOption(data: ChartItem[]): echarts.EChartsOption {
   const names = data.map((item) => item.name)
@@ -53,7 +56,7 @@ function getChartOption(data: ChartItem[]): echarts.EChartsOption {
     },
     yAxis: {
       type: 'value',
-      name: '累计注气量()',
+      name: '累计注气量(万方)',
       nameTextStyle: {
         color: THEME.text.regular,
         fontSize: 13,
@@ -137,9 +140,36 @@ function initChart() {
   chart = echarts.init(chartRef.value, undefined, {
     renderer: 'svg'
   })
+  chart.getZr().on('click', handleChartClick)
+  chartClickBound = true
   renderChart()
 }
 
+function getChartMonthRange() {
+  const startDate = dayjs(chartData.value[0]?.name)
+  const endDate = dayjs(chartData.value[chartData.value.length - 1]?.name)
+  if (!startDate.isValid() || !endDate.isValid()) return null
+
+  return [
+    startDate.startOf('month').format('YYYY-MM-DD HH:mm:ss'),
+    endDate.endOf('month').format('YYYY-MM-DD HH:mm:ss')
+  ]
+}
+
+function handleChartClick() {
+  const createTime = getChartMonthRange()
+  if (!createTime) return
+
+  router.push({
+    name: 'IotRhDailyReportSummary',
+    query: {
+      activeTab: '日报统计',
+      view: 'kanban',
+      createTime
+    }
+  })
+}
+
 function renderChart() {
   if (!chart) return
 
@@ -154,6 +184,10 @@ function resizeChart() {
 
 function destroyChart() {
   if (chart) {
+    if (chartClickBound) {
+      chart.getZr().off('click', handleChartClick)
+      chartClickBound = false
+    }
     chart.dispose()
     chart = null
   }

+ 507 - 0
src/views/pms/stat/rhkb/inventorySituation.vue

@@ -0,0 +1,507 @@
+<script lang="ts" setup>
+import * as echarts from 'echarts'
+import {
+  ANIMATION,
+  CHART_RENDERER,
+  createLegend,
+  createTooltip,
+  FONT_FAMILY,
+  THEME
+} from '@/utils/kb'
+
+type ActivePanel = 'distribution' | 'trend'
+
+type InventoryItem = {
+  project: string
+  mayInventoryAmount: number
+  yearBeginningBacklog: number
+  mayBacklogAmount: number
+}
+
+const activePanel = ref<ActivePanel>('distribution')
+const distributionChartRef = ref<HTMLDivElement>()
+const trendChartRef = ref<HTMLDivElement>()
+
+let distributionChart: echarts.ECharts | null = null
+let trendChart: echarts.ECharts | null = null
+
+const panelOptions: Array<{ label: string; value: ActivePanel }> = [
+  { label: '分布', value: 'distribution' },
+  { label: '趋势', value: 'trend' }
+]
+
+const yuanToWan = (value: number) => Number((value / 10000).toFixed(2))
+
+const inventoryData: InventoryItem[] = [
+  {
+    project: '东营库',
+    mayInventoryAmount: yuanToWan(2921827.56),
+    yearBeginningBacklog: yuanToWan(1644059.52),
+    mayBacklogAmount: yuanToWan(1436126.52)
+  },
+  {
+    project: '川庆',
+    mayInventoryAmount: yuanToWan(491050.94),
+    yearBeginningBacklog: 0,
+    mayBacklogAmount: yuanToWan(308801.56)
+  },
+  {
+    project: '南美',
+    mayInventoryAmount: yuanToWan(2491993.31),
+    yearBeginningBacklog: 0,
+    mayBacklogAmount: 0
+  },
+  {
+    project: '非洲',
+    mayInventoryAmount: yuanToWan(2276836.24),
+    yearBeginningBacklog: yuanToWan(175144.46),
+    mayBacklogAmount: yuanToWan(391697.89)
+  },
+  {
+    project: '中东',
+    mayInventoryAmount: yuanToWan(127300.64),
+    yearBeginningBacklog: yuanToWan(14784.43),
+    mayBacklogAmount: yuanToWan(103461.07)
+  },
+  {
+    project: '中亚',
+    mayInventoryAmount: yuanToWan(188389.42),
+    yearBeginningBacklog: 0,
+    mayBacklogAmount: 0
+  },
+  {
+    project: '塔河库',
+    mayInventoryAmount: yuanToWan(17175342.26),
+    yearBeginningBacklog: yuanToWan(9890389.23),
+    mayBacklogAmount: yuanToWan(9261631.2)
+  },
+  {
+    project: '塔里木',
+    mayInventoryAmount: yuanToWan(4599437.38),
+    yearBeginningBacklog: 0,
+    mayBacklogAmount: yuanToWan(890890.45)
+  },
+  {
+    project: '吐哈库',
+    mayInventoryAmount: yuanToWan(2342089.06),
+    yearBeginningBacklog: yuanToWan(1097925.36),
+    mayBacklogAmount: yuanToWan(1190879.22)
+  }
+]
+
+const activeTitle = computed(() => (activePanel.value === 'distribution' ? '存货分布' : '积压趋势'))
+
+function formatAmount(value: number) {
+  return Number(value || 0).toFixed(2)
+}
+
+function getChartLayout(chartRef: Ref<HTMLDivElement | undefined>) {
+  const { clientWidth = 0, clientHeight = 0 } = chartRef.value ?? {}
+  const compact = clientHeight > 0 && (clientHeight < 210 || clientWidth < 520)
+
+  return {
+    compact,
+    distributionTitleTop: compact ? 6 : 18,
+    distributionTitleFontSize: compact ? 12 : 14,
+    distributionTitleLineHeight: compact ? 14 : 16,
+    distributionPieRadius: compact ? ['36%', '54%'] : ['48%', '68%'],
+    distributionPieCenterY: compact ? '62%' : '57%',
+    distributionLegendBottom: compact ? 0 : 10,
+    distributionLegendItemSize: compact ? 9 : 13,
+    distributionLegendGap: compact ? 8 : 14,
+    distributionLegendFontSize: compact ? 10 : 14,
+    trendGridTop: compact ? 10 : 32,
+    trendGridRight: compact ? 12 : THEME.grid.right,
+    trendGridBottom: compact ? 2 : THEME.grid.bottom,
+    legendTop: compact ? 0 : 4,
+    legendRight: compact ? 2 : 6,
+    legendItemSize: compact ? 9 : 12,
+    legendGap: compact ? 10 : 16,
+    legendFontSize: compact ? 11 : 14,
+    axisFontSize: compact ? 10 : 12,
+    yAxisLabelWidth: compact ? 72 : 96,
+    yAxisLabelMargin: compact ? 8 : 12,
+    barWidth: compact ? 8 : 12,
+    barGap: compact ? '12%' : '5%',
+    barCategoryGap: compact ? '46%' : '36%',
+    labelDistance: compact ? 4 : 7,
+    labelFontSize: compact ? 10 : 12
+  }
+}
+
+function getDistributionOption(data: InventoryItem[]): echarts.EChartsOption {
+  const layout = getChartLayout(distributionChartRef)
+  const inventoryTotal = data.reduce((total, item) => total + item.mayInventoryAmount, 0)
+  const backlogTotal = data.reduce((total, item) => total + item.mayBacklogAmount, 0)
+
+  return {
+    ...ANIMATION,
+    color: [
+      THEME.color.blue.line,
+      THEME.color.orange.line,
+      THEME.color.green.line,
+      THEME.color.red.line,
+      THEME.color.blue.mid,
+      THEME.color.orange.mid,
+      THEME.color.green.mid,
+      THEME.color.red.mid,
+      THEME.color.blue.light
+    ],
+    tooltip: createTooltip({
+      trigger: 'item',
+      formatter(params: any) {
+        return `${params.seriesName}<br/>${params.marker}${params.name}:${formatAmount(
+          params.value
+        )}万元<br/>占比:${params.percent}%`
+      }
+    }),
+    title: [
+      {
+        text: `5月底余额\n${formatAmount(inventoryTotal)} 万元`,
+        left: '26.5%',
+        top: layout.distributionTitleTop,
+        textAlign: 'center',
+        textStyle: {
+          color: THEME.text.strong,
+          fontSize: layout.distributionTitleFontSize,
+          fontWeight: 700,
+          lineHeight: layout.distributionTitleLineHeight,
+          fontFamily: FONT_FAMILY
+        }
+      },
+      {
+        text: `5月底积压\n${formatAmount(backlogTotal)} 万元`,
+        left: '72.5%',
+        top: layout.distributionTitleTop,
+        textAlign: 'center',
+        textStyle: {
+          color: THEME.text.strong,
+          fontSize: layout.distributionTitleFontSize,
+          fontWeight: 700,
+          lineHeight: layout.distributionTitleLineHeight,
+          fontFamily: FONT_FAMILY
+        }
+      }
+    ],
+    legend: createLegend({
+      type: 'scroll',
+      bottom: layout.distributionLegendBottom,
+      left: 10,
+      right: 10,
+      itemWidth: layout.distributionLegendItemSize,
+      itemHeight: layout.distributionLegendItemSize,
+      itemGap: layout.distributionLegendGap,
+      textStyle: {
+        color: THEME.text.regular,
+        fontSize: layout.distributionLegendFontSize,
+        fontWeight: 600,
+        fontFamily: FONT_FAMILY
+      }
+    }),
+    series: [
+      {
+        name: '5月底余额',
+        type: 'pie',
+        radius: layout.distributionPieRadius,
+        center: ['27%', layout.distributionPieCenterY],
+        minAngle: 5,
+        data: data.map((item) => ({
+          name: item.project,
+          value: item.mayInventoryAmount
+        }))
+      },
+      {
+        name: '5月底积压',
+        type: 'pie',
+        radius: layout.distributionPieRadius,
+        center: ['73%', layout.distributionPieCenterY],
+        minAngle: 5,
+        data: data
+          .filter((item) => item.mayBacklogAmount > 0)
+          .map((item) => ({
+            name: item.project,
+            value: item.mayBacklogAmount
+          }))
+      }
+    ]
+  }
+}
+
+function getTrendOption(data: InventoryItem[]): echarts.EChartsOption {
+  const layout = getChartLayout(trendChartRef)
+  const maxBacklog = Math.max(
+    ...data.map((item) => Math.max(item.yearBeginningBacklog, item.mayBacklogAmount)),
+    1
+  )
+  const backlogAxisMax = Math.ceil((maxBacklog * 1.15) / 100) * 100
+  const barLabel = {
+    show: true,
+    position: 'right' as any,
+    distance: layout.labelDistance,
+    color: THEME.text.strong,
+    fontSize: layout.labelFontSize,
+    fontWeight: 700,
+    fontFamily: FONT_FAMILY,
+    formatter(params: any) {
+      return formatAmount(Number(params.value))
+    }
+  }
+
+  return {
+    ...ANIMATION,
+    grid: {
+      ...THEME.grid,
+      top: layout.trendGridTop,
+      right: layout.trendGridRight,
+      bottom: layout.trendGridBottom
+    },
+    color: [THEME.color.blue.line, THEME.color.orange.line],
+    legend: createLegend(
+      {
+        top: layout.legendTop,
+        right: layout.legendRight,
+        itemWidth: layout.legendItemSize,
+        itemHeight: layout.legendItemSize,
+        itemGap: layout.legendGap,
+        textStyle: {
+          color: THEME.text.regular,
+          fontSize: layout.legendFontSize,
+          fontWeight: 600,
+          fontFamily: FONT_FAMILY
+        }
+      },
+      ['年初积压', '5月底积压']
+    ),
+    tooltip: createTooltip({
+      trigger: 'axis',
+      axisPointer: {
+        type: 'shadow',
+        shadowStyle: {
+          color: THEME.split
+        }
+      },
+      valueFormatter(value: number) {
+        return `${formatAmount(value)}万元`
+      }
+    }),
+    xAxis: {
+      type: 'value',
+      max: backlogAxisMax,
+      splitNumber: 4,
+      axisLine: {
+        show: false
+      },
+      axisTick: {
+        show: false
+      },
+      axisLabel: {
+        color: THEME.text.regular,
+        fontSize: layout.axisFontSize,
+        fontFamily: FONT_FAMILY
+      },
+      splitLine: {
+        lineStyle: {
+          color: THEME.split,
+          type: 'dashed'
+        }
+      }
+    },
+    yAxis: {
+      type: 'category',
+      data: data.map((item) => item.project),
+      inverse: true,
+      axisLine: {
+        show: false
+      },
+      axisTick: {
+        show: false
+      },
+      axisLabel: {
+        color: THEME.text.regular,
+        fontSize: layout.axisFontSize,
+        fontWeight: 600,
+        fontFamily: FONT_FAMILY,
+        margin: layout.yAxisLabelMargin,
+        width: layout.yAxisLabelWidth,
+        overflow: 'break',
+        align: 'right'
+      }
+    },
+    series: [
+      {
+        name: '年初积压',
+        type: 'bar',
+        data: data.map((item) => item.yearBeginningBacklog),
+        barWidth: layout.barWidth,
+        barGap: layout.barGap,
+        barCategoryGap: layout.barCategoryGap,
+        label: barLabel,
+        labelLayout: {
+          hideOverlap: false
+        },
+        itemStyle: {
+          shadowBlur: 10,
+          shadowColor: THEME.color.blue.bg,
+          color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
+            { offset: 0, color: THEME.color.blue.light },
+            { offset: 0.55, color: THEME.color.blue.mid },
+            { offset: 1, color: THEME.color.blue.line }
+          ])
+        }
+      },
+      {
+        name: '5月底积压',
+        type: 'bar',
+        data: data.map((item) => item.mayBacklogAmount),
+        barWidth: layout.barWidth,
+        barGap: layout.barGap,
+        barCategoryGap: layout.barCategoryGap,
+        label: barLabel,
+        labelLayout: {
+          hideOverlap: false
+        },
+        itemStyle: {
+          shadowBlur: 10,
+          shadowColor: THEME.color.orange.bg,
+          color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
+            { offset: 0, color: THEME.color.orange.light },
+            { offset: 0.55, color: THEME.color.orange.mid },
+            { offset: 1, color: THEME.color.orange.line }
+          ])
+        }
+      }
+    ]
+  }
+}
+
+function initChart(
+  chartRef: Ref<HTMLDivElement | undefined>,
+  chart: echarts.ECharts | null,
+  option: echarts.EChartsOption
+) {
+  if (!chartRef.value) return chart
+
+  chart?.dispose()
+  const nextChart = echarts.init(chartRef.value, undefined, {
+    renderer: CHART_RENDERER
+  })
+  nextChart.setOption(option, true)
+
+  return nextChart
+}
+
+function renderDistributionChart() {
+  distributionChart?.setOption(getDistributionOption(inventoryData), true)
+}
+
+function renderTrendChart() {
+  trendChart?.setOption(getTrendOption(inventoryData), true)
+}
+
+function initDistributionChart() {
+  distributionChart = initChart(
+    distributionChartRef,
+    distributionChart,
+    getDistributionOption(inventoryData)
+  )
+}
+
+function initTrendChart() {
+  trendChart = initChart(trendChartRef, trendChart, getTrendOption(inventoryData))
+}
+
+function resizeCharts() {
+  distributionChart?.resize()
+  trendChart?.resize()
+  renderDistributionChart()
+  renderTrendChart()
+}
+
+function destroyCharts() {
+  distributionChart?.dispose()
+  trendChart?.dispose()
+  distributionChart = null
+  trendChart = null
+}
+
+watch(activePanel, (value) => {
+  nextTick(() => {
+    if (value === 'distribution') {
+      if (!distributionChart) initDistributionChart()
+      renderDistributionChart()
+    } else {
+      if (!trendChart) initTrendChart()
+      renderTrendChart()
+    }
+    resizeCharts()
+  })
+})
+
+onMounted(() => {
+  initDistributionChart()
+  window.addEventListener('resize', resizeCharts)
+  window.addEventListener('rhkb:resize', resizeCharts)
+})
+
+onUnmounted(() => {
+  window.removeEventListener('resize', resizeCharts)
+  window.removeEventListener('rhkb:resize', resizeCharts)
+  destroyCharts()
+})
+</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>
+        {{ activeTitle }}
+      </div>
+      <el-segmented
+        v-model="activePanel"
+        :options="panelOptions"
+        size="small"
+        class="inventory-switch" />
+    </div>
+    <div class="flex-1 min-h-0">
+      <div
+        v-show="activePanel === 'distribution'"
+        ref="distributionChartRef"
+        class="inventory-chart"></div>
+      <div v-show="activePanel === 'trend'" ref="trendChartRef" class="inventory-chart"></div>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+@import url('@/styles/kb.scss');
+
+.inventory-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(8px * var(--kb-scale, 1));
+    font-size: calc(13px * var(--kb-scale, 1));
+    font-weight: 600;
+    color: #29527f;
+  }
+}
+
+.inventory-chart {
+  width: 100%;
+  height: 100%;
+  min-height: 0;
+}
+</style>

+ 29 - 0
src/views/pms/stat/rhkb/operation.vue

@@ -1,5 +1,6 @@
 <script lang="ts" setup>
 import * as echarts from 'echarts'
+import dayjs from 'dayjs'
 import {
   ANIMATION,
   ChartItem,
@@ -12,8 +13,10 @@ import { IotStatApi } from '@/api/pms/stat'
 
 const chartData = ref<ChartItem[]>([])
 
+const router = useRouter()
 const chartRef = ref<HTMLDivElement>()
 let chart: echarts.ECharts | null = null
+let chartClickBound = false
 
 function getChartOption(data: ChartItem[]): echarts.EChartsOption {
   const names = data.map((item) => item.name)
@@ -129,6 +132,26 @@ function initChart() {
   renderChart()
 }
 
+function getChartDateRange() {
+  const startDate = dayjs(chartData.value[0]?.name)
+  const endDate = dayjs(chartData.value[chartData.value.length - 1]?.name)
+  if (!startDate.isValid() || !endDate.isValid()) return undefined
+
+  return [
+    startDate.startOf('day').format('YYYY-MM-DD HH:mm:ss'),
+    endDate.endOf('day').format('YYYY-MM-DD HH:mm:ss')
+  ]
+}
+
+function handleChartClick() {
+  router.push({
+    path: '/report-statistics/operational-costs',
+    query: {
+      createTime: getChartDateRange()
+    }
+  })
+}
+
 function renderChart() {
   if (!chart) return
 
@@ -143,6 +166,10 @@ function resizeChart() {
 
 function destroyChart() {
   if (chart) {
+    if (chartClickBound) {
+      chart.getZr().off('click', handleChartClick)
+      chartClickBound = false
+    }
     chart.dispose()
     chart = null
   }
@@ -165,6 +192,8 @@ async function loadChart() {
 
 onMounted(() => {
   initChart()
+  chart?.getZr().on('click', handleChartClick)
+  chartClickBound = true
   loadChart()
   window.addEventListener('resize', resizeChart)
   window.addEventListener('rhkb:resize', resizeChart)

+ 163 - 0
src/views/pms/stat/rhkb/rhsafeday.vue

@@ -0,0 +1,163 @@
+<script lang="ts" setup>
+import CountTo from '@/components/count-to1.vue'
+import { IotStatApi } from '@/api/pms/stat'
+
+const FIXED_SAFE_DAYS = 123
+
+const safeDays = ref(FIXED_SAFE_DAYS)
+
+async function loadSafeDays() {
+  try {
+    const res = await IotStatApi.getSafeCount1(157)
+    safeDays.value = res
+  } catch (error) {
+    console.error('获取安全生产天数失败:', error)
+  } finally {
+    // safeDays.value = FIXED_SAFE_DAYS
+  }
+}
+
+onMounted(() => {
+  loadSafeDays()
+})
+</script>
+
+<template>
+  <div class="panel flex flex-col">
+    <div class="panel-title">
+      <div class="icon-decorator">
+        <span></span>
+        <span></span>
+      </div>
+      安全生产天数
+    </div>
+
+    <div class="safe-day-panel flex-1 min-h-0">
+      <div class="safe-day-panel__orbit safe-day-panel__orbit--outer"></div>
+
+      <div class="safe-day-panel__top">
+        <span class="safe-day-panel__tag">SAFE RUN</span>
+      </div>
+
+      <div class="safe-day-panel__center">
+        <div class="safe-day-panel__headline">连续安全生产</div>
+        <div class="safe-day-panel__value-row translate-x-1.5">
+          <CountTo
+            class="safe-day-panel__value"
+            :start-val="0"
+            :end-val="safeDays"
+            :duration="1200" />
+          <span class="safe-day-panel__unit">天</span>
+        </div>
+        <!-- <div class="safe-day-panel__subline">截至当前未发生安全生产事故</div> -->
+      </div>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+@import url('@/styles/kb.scss');
+
+.safe-day-panel {
+  position: relative;
+  display: flex;
+  padding: calc(22px * var(--kb-scale, 1)) calc(24px * var(--kb-scale, 1))
+    calc(18px * var(--kb-scale, 1));
+  overflow: hidden;
+  flex-direction: column;
+}
+
+.safe-day-panel__orbit {
+  position: absolute;
+  left: 50%;
+  border: 1px solid rgb(31 91 184 / 15%);
+  border-radius: 999px;
+  transform: translateX(-50%);
+}
+
+.safe-day-panel__orbit--outer {
+  top: calc(32px * var(--kb-scale, 1));
+  width: calc(320px * var(--kb-scale, 1));
+  height: calc(320px * var(--kb-scale, 1));
+}
+
+.safe-day-panel__orbit--inner {
+  top: calc(62px * var(--kb-scale, 1));
+  width: calc(240px * var(--kb-scale, 1));
+  height: calc(240px * var(--kb-scale, 1));
+  border-style: dashed;
+}
+
+.safe-day-panel__top,
+.safe-day-panel__center {
+  position: relative;
+  z-index: 1;
+}
+
+.safe-day-panel__top {
+  display: flex;
+  align-items: center;
+  justify-content: flex-start;
+}
+
+.safe-day-panel__tag {
+  padding: calc(5px * var(--kb-scale, 1)) calc(12px * var(--kb-scale, 1));
+  font-family: YouSheBiaoTiHei, sans-serif;
+  font-size: calc(16px * var(--kb-scale, 1));
+  line-height: 1;
+  letter-spacing: 1px;
+  color: #1f5bb8;
+  background: rgb(255 255 255 / 72%);
+  border: 1px solid rgb(255 255 255 / 75%);
+  border-radius: 999px;
+  box-shadow: inset 0 1px 0 rgb(255 255 255 / 85%);
+}
+
+.safe-day-panel__center {
+  display: flex;
+  padding-top: calc(14px * var(--kb-scale, 1));
+  margin-top: auto;
+  margin-bottom: auto;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  text-align: center;
+}
+
+.safe-day-panel__headline {
+  font-size: calc(20px * var(--kb-scale, 1));
+  font-weight: 600;
+  letter-spacing: 1px;
+  color: #24364f;
+}
+
+.safe-day-panel__value-row {
+  display: flex;
+  margin-top: calc(10px * var(--kb-scale, 1));
+  align-items: flex-end;
+  justify-content: center;
+}
+
+.safe-day-panel__value {
+  font-family: YouSheBiaoTiHei, sans-serif;
+  font-size: calc(82px * var(--kb-scale, 1));
+  line-height: 0.9;
+  letter-spacing: 2px;
+  color: #1f5bb8;
+  text-shadow: 0 12px 24px rgb(31 91 184 / 12%);
+}
+
+.safe-day-panel__unit {
+  padding-bottom: calc(10px * var(--kb-scale, 1));
+  font-family: YouSheBiaoTiHei, sans-serif;
+  font-size: calc(30px * var(--kb-scale, 1));
+  line-height: 1;
+  color: #f08c2e;
+}
+
+.safe-day-panel__subline {
+  margin-top: calc(10px * var(--kb-scale, 1));
+  font-size: calc(15px * var(--kb-scale, 1));
+  color: #6f85aa;
+}
+</style>

+ 149 - 32
src/views/pms/stat/rhkb/rhsummary.vue

@@ -5,6 +5,9 @@ import dayjs from 'dayjs'
 
 type CardKey =
   | 'device'
+  | 'utilizationRate'
+  | 'todayGas'
+  | 'yearGas'
   | 'maintain'
   | 'unfilledCount'
   | 'filledCount'
@@ -24,12 +27,33 @@ const cardConfigs: CardConfig[] = [
     glow: THEME.color.blue.glow
   },
   {
-    key: 'maintain',
-    title: '维修工单',
-    icon: 'i-material-symbols:home-repair-service',
-    accent: THEME.color.orange.strong,
-    glow: THEME.color.orange.glow
+    key: 'utilizationRate',
+    title: '设备利用率(%)',
+    icon: 'i-solar:chart-2-linear',
+    accent: THEME.color.green.strong,
+    glow: THEME.color.green.glow
+  },
+  {
+    key: 'todayGas',
+    title: '当日注气量(万方)',
+    icon: 'i-solar:gas-station-linear',
+    accent: THEME.color.blue.strong,
+    glow: THEME.color.blue.glow
   },
+  {
+    key: 'yearGas',
+    title: '累计注气量(万方)',
+    icon: 'i-solar:database-linear',
+    accent: THEME.color.green.strong,
+    glow: THEME.color.green.glow
+  },
+  // {
+  //   key: 'maintain',
+  //   title: '维修工单',
+  //   icon: 'i-material-symbols:home-repair-service',
+  //   accent: THEME.color.orange.strong,
+  //   glow: THEME.color.orange.glow
+  // },
   {
     key: 'unfilledCount',
     title: '运行未填写',
@@ -37,13 +61,13 @@ const cardConfigs: CardConfig[] = [
     accent: THEME.color.orange.strong,
     glow: THEME.color.orange.glow
   },
-  {
-    key: 'filledCount',
-    title: '运行已填写',
-    icon: 'i-solar:clipboard-check-linear',
-    accent: THEME.color.green.strong,
-    glow: THEME.color.green.glow
-  },
+  // {
+  //   key: 'filledCount',
+  //   title: '运行已填写',
+  //   icon: 'i-solar:clipboard-check-linear',
+  //   accent: THEME.color.green.strong,
+  //   glow: THEME.color.green.glow
+  // },
   {
     key: 'bytodo',
     title: '待保养',
@@ -51,32 +75,35 @@ const cardConfigs: CardConfig[] = [
     accent: THEME.color.orange.strong,
     glow: THEME.color.orange.glow
   },
-  {
-    key: 'byfinished',
-    title: '已保养',
-    icon: 'i-solar:shield-check-linear',
-    accent: THEME.color.green.strong,
-    glow: THEME.color.green.glow
-  },
+  // {
+  //   key: 'byfinished',
+  //   title: '已保养',
+  //   icon: 'i-solar:shield-check-linear',
+  //   accent: THEME.color.green.strong,
+  //   glow: THEME.color.green.glow
+  // },
   {
     key: 'inspectttodo',
     title: '待巡检',
     icon: 'i-solar:map-point-search-linear',
     accent: THEME.color.orange.strong,
     glow: THEME.color.orange.glow
-  },
-  {
-    key: 'inspecttfinished',
-    title: '已巡检',
-    icon: 'i-solar:check-circle-linear',
-    accent: THEME.color.green.strong,
-    glow: THEME.color.green.glow
   }
+  // {
+  //   key: 'inspecttfinished',
+  //   title: '已巡检',
+  //   icon: 'i-solar:check-circle-linear',
+  //   accent: THEME.color.green.strong,
+  //   glow: THEME.color.green.glow
+  // }
 ]
 
 function createDefaultCardState(): Record<CardKey, CardStateItem> {
   return {
     device: { value: 0, loading: true },
+    utilizationRate: { value: 0, loading: true },
+    todayGas: { value: 0, loading: true },
+    yearGas: { value: 0, loading: true },
     maintain: { value: 0, loading: true },
     unfilledCount: { value: 0, loading: true },
     filledCount: { value: 0, loading: true },
@@ -93,7 +120,8 @@ const summaryCards = computed(() =>
   cardConfigs.map((card) => ({
     ...card,
     value: cardState[card.key].value,
-    loading: cardState[card.key].loading
+    loading: cardState[card.key].loading,
+    decimals: ['utilizationRate', 'todayGas', 'yearGas'].includes(card.key) ? 2 : 0
   }))
 )
 
@@ -102,7 +130,51 @@ function toNumber(value: unknown) {
   return Number.isFinite(num) ? num : 0
 }
 
+function getCreateTimeRange(start: dayjs.Dayjs, end: dayjs.Dayjs) {
+  return [start.format('YYYY-MM-DD HH:mm:ss'), end.format('YYYY-MM-DD HH:mm:ss')]
+}
+
+function getFirstNumber(data: unknown): number {
+  if (typeof data === 'number' || typeof data === 'string') return toNumber(data)
+  if (!data || typeof data !== 'object') return 0
+
+  const record = data as Record<string, unknown>
+  const preferredKeys = [
+    'total',
+    'value',
+    'rate',
+    'utilizationRate',
+    'totalUtilizationRate',
+    'gas',
+    'zql',
+    'gasInjection',
+    'totalGasInjection',
+    'totalNaturalGasInjection',
+    'totalN2GasInjection'
+  ]
+
+  for (const key of preferredKeys) {
+    if (record[key] !== undefined) return getFirstNumber(record[key])
+  }
+
+  if (Array.isArray(data)) {
+    return getFirstNumber(data[0])
+  }
+
+  const firstNumber = Object.values(record).find(
+    (value) => Number.isFinite(Number(value)) && value !== ''
+  )
+
+  return toNumber(firstNumber)
+}
+
+function normalizePercent(value: unknown) {
+  const num = getFirstNumber(value)
+  return Math.abs(num) <= 1 ? num * 100 : num
+}
+
 function setCardValue(key: CardKey, value: unknown) {
+  console.log(key, value)
   cardState[key].value = toNumber(value)
 }
 
@@ -127,6 +199,50 @@ async function loadDeviceCard() {
   }
 }
 
+async function loadUtilizationRateCard() {
+  const keys: CardKey[] = ['utilizationRate']
+  setCardLoading(keys, true)
+
+  const params = {
+    createTime: getCreateTimeRange(dayjs().startOf('year'), dayjs().endOf('day'))
+  }
+
+  try {
+    const res = await IotStatApi.getRhTotalUtilizationRate(params)
+    setCardValue('utilizationRate', normalizePercent(res))
+  } catch (error) {
+    console.error('获取设备利用率失败:', error)
+    setCardValue('utilizationRate', 0)
+  } finally {
+    setCardLoading(keys, false)
+  }
+}
+
+async function loadGasCards() {
+  const keys: CardKey[] = ['todayGas', 'yearGas']
+  setCardLoading(keys, true)
+
+  try {
+    const [todayRes, yearRes] = await Promise.all([
+      IotStatApi.getRhZqlTotal({
+        createTime: getCreateTimeRange(dayjs().startOf('day'), dayjs().endOf('day'))
+      }),
+      IotStatApi.getRhZqlTotal({
+        createTime: getCreateTimeRange(dayjs().startOf('year'), dayjs().endOf('day'))
+      })
+    ])
+
+    setCardValue('todayGas', todayRes)
+    setCardValue('yearGas', yearRes)
+  } catch (error) {
+    console.error('获取注气量失败:', error)
+    setCardValue('todayGas', 0)
+    setCardValue('yearGas', 0)
+  } finally {
+    setCardLoading(keys, false)
+  }
+}
+
 async function loadMaintainCard() {
   const keys: CardKey[] = ['maintain']
   setCardLoading(keys, true)
@@ -212,6 +328,8 @@ async function loadInspectCards() {
 
 function loadAllCards() {
   loadDeviceCard()
+  loadUtilizationRateCard()
+  loadGasCards()
   loadMaintainCard()
   loadFillCards()
   loadMaintainStatusCards()
@@ -233,7 +351,7 @@ onMounted(() => {
       工单情况
     </div>
 
-    <div class="summary-panel__grid grid grid-cols-8 flex-1">
+    <div class="summary-panel__grid grid grid-cols-7 flex-1">
       <article
         v-for="card in summaryCards"
         :key="card.key"
@@ -241,8 +359,7 @@ onMounted(() => {
         :style="{
           '--card-accent': card.accent,
           '--card-glow': card.glow
-        }"
-      >
+        }">
         <div class="summary-card__shine"></div>
 
         <div class="summary-card__icon">
@@ -257,8 +374,8 @@ onMounted(() => {
               style="color: #1f5bb8"
               :start-val="0"
               :end-val="card.value"
-              :duration="1200"
-            />
+              :decimals="card.decimals"
+              :duration="1200" />
             <span v-else class="summary-card__placeholder">--</span>
           </div>
         </div>

+ 36 - 1
src/views/pms/stat/rhkb/todayGas.vue

@@ -1,5 +1,6 @@
 <script lang="ts" setup>
 import * as echarts from 'echarts'
+import dayjs from 'dayjs'
 import {
   ANIMATION,
   ChartData,
@@ -16,8 +17,10 @@ const chartData = ref<ChartData>({
   series: []
 })
 
+const router = useRouter()
 const chartRef = ref<HTMLDivElement>()
 let chart: echarts.ECharts | null = null
+let chartClickBound = false
 
 function getChartOption(data: ChartData): echarts.EChartsOption {
   const xAxisData = data.xAxis || []
@@ -83,7 +86,7 @@ function getChartOption(data: ChartData): echarts.EChartsOption {
     },
     yAxis: {
       type: 'value',
-      name: '当日注气量()',
+      name: '当日注气量(万方)',
       nameTextStyle: {
         color: THEME.text.regular,
         fontSize: 13,
@@ -144,9 +147,37 @@ function initChart() {
   chart = echarts.init(chartRef.value, undefined, {
     renderer: 'svg'
   })
+  chart.getZr().on('click', handleChartClick)
+  chartClickBound = true
   renderChart()
 }
 
+function getChartDayRange() {
+  const xAxis = chartData.value.xAxis || []
+  const startDate = dayjs(xAxis[0])
+  const endDate = dayjs(xAxis[xAxis.length - 1])
+  if (!startDate.isValid() || !endDate.isValid()) return null
+
+  return [
+    startDate.startOf('day').format('YYYY-MM-DD HH:mm:ss'),
+    endDate.endOf('day').format('YYYY-MM-DD HH:mm:ss')
+  ]
+}
+
+function handleChartClick() {
+  const createTime = getChartDayRange()
+  if (!createTime) return
+
+  router.push({
+    name: 'IotRhDailyReportSummary',
+    query: {
+      activeTab: '日报统计',
+      view: 'kanban',
+      createTime
+    }
+  })
+}
+
 function renderChart() {
   if (!chart) return
 
@@ -161,6 +192,10 @@ function resizeChart() {
 
 function destroyChart() {
   if (chart) {
+    if (chartClickBound) {
+      chart.getZr().off('click', handleChartClick)
+      chartClickBound = false
+    }
     chart.dispose()
     chart = null
   }

+ 17 - 10
src/views/pms/stat/rykb.vue

@@ -7,7 +7,10 @@ import zjStatsSwitch from './rykb/zjStatsSwitch.vue'
 import xjwork from './rykb/xjwork.vue'
 import rydeviceList from './rykb/rydeviceList.vue'
 import ryProductionBriefs from './rykb/ryProductionBriefs.vue'
-import rydeviceType from './rykb/rydeviceType.vue'
+import equipmentCategory from './rykb/equipment-category.vue'
+import exceptionPrompt from './rykb/exception-prompt.vue'
+import historicalWorkload from './rykb/historical-workload.vue'
+import inventorySituation from './rykb/inventory-situation.vue'
 
 defineOptions({
   name: 'IotRyStatt'
@@ -102,12 +105,17 @@ onUnmounted(() => {
           <div v-if="activePage === 'home'" class="kb-home-page">
             <rysummary class="kb-stage-card kb-stage-card--1" />
             <div class="kb-chart-grid">
+              <equipmentCategory class="kb-stage-card kb-stage-card--5" />
+              <zjStatsSwitch class="kb-stage-card kb-stage-card--3" />
+              <safeday class="kb-stage-card kb-stage-card--4" />
               <rydeviceStatus class="kb-stage-card kb-stage-card--2" />
-              <rydeviceType class="kb-stage-card kb-stage-card--6" />
-              <rydeviceList class="kb-stage-card kb-stage-card--4 kb-stage-card--list" />
-              <safeday class="kb-stage-card kb-stage-card--3" />
-              <zjStatsSwitch class="kb-stage-card kb-stage-card--5" />
-              <xjwork class="kb-stage-card kb-stage-card--7" />
+              <xjwork class="kb-stage-card kb-stage-card--6" />
+              <exceptionPrompt class="kb-stage-card kb-stage-card--7" />
+              <rydeviceList class="kb-stage-card kb-stage-card--8 kb-stage-card--list" />
+              <historicalWorkload class="kb-stage-card kb-stage-card--9" />
+              <inventorySituation class="kb-stage-card kb-stage-card--10" />
+
+              <!-- <rydeviceType class="kb-stage-card kb-stage-card--6" /> -->
             </div>
           </div>
 
@@ -132,8 +140,7 @@ onUnmounted(() => {
 .kb-content {
   position: relative;
   height: calc(100% - 52px * var(--kb-scale));
-  padding: calc(44px * var(--kb-scale)) calc(20px * var(--kb-scale))
-    calc(20px * var(--kb-scale));
+  padding: calc(44px * var(--kb-scale)) calc(20px * var(--kb-scale)) calc(20px * var(--kb-scale));
 }
 
 .page-tabs {
@@ -147,8 +154,8 @@ onUnmounted(() => {
 }
 
 .page-tab {
-  min-width: calc(82px * var(--kb-scale));
   height: calc(28px * var(--kb-scale));
+  min-width: calc(82px * var(--kb-scale));
   padding: 0 calc(14px * var(--kb-scale));
   font-family: YouSheBiaoTiHei, sans-serif;
   font-size: calc(15px * var(--kb-scale));
@@ -194,7 +201,7 @@ onUnmounted(() => {
   flex: 1;
   margin-top: calc(12px * var(--kb-scale));
   gap: calc(12px * var(--kb-scale));
-  grid-template-rows: repeat(2, minmax(0, 1fr));
+  grid-template-rows: repeat(3, minmax(0, 1fr));
   grid-template-columns: repeat(3, minmax(0, 1fr));
 }
 

+ 397 - 0
src/views/pms/stat/rykb/equipment-category.vue

@@ -0,0 +1,397 @@
+<script lang="ts" setup>
+import * as echarts from 'echarts'
+import {
+  ANIMATION,
+  CHART_RENDERER,
+  createLegend,
+  createTooltip,
+  FONT_FAMILY,
+  THEME
+} from '@/utils/kb'
+
+type ActivePanel = 'category' | 'value'
+
+type EquipmentCategoryItem = {
+  project: string
+  model: string
+  total: number
+  owned: number
+}
+
+const activePanel = ref<ActivePanel>('category')
+const categoryChartRef = ref<HTMLDivElement>()
+let categoryChart: echarts.ECharts | null = null
+
+const panelOptions: Array<{ label: string; value: ActivePanel }> = [
+  {
+    label: '类别',
+    value: 'category'
+  },
+  {
+    label: '价值',
+    value: 'value'
+  }
+]
+
+const activeTitle = computed(() =>
+  activePanel.value === 'category' ? '设备类别统计' : '设备价值统计'
+)
+
+const categoryData: EquipmentCategoryItem[] = [
+  { project: 'SCP项目', model: '50L', total: 4, owned: 3 },
+  { project: '紫金山项目', model: '40J', total: 2, owned: 1 },
+  { project: '新疆修井项目', model: '550HP', total: 9, owned: 9 },
+  { project: '新疆修井项目', model: '650HP', total: 1, owned: 1 },
+  { project: '伊拉克钻修项目', model: '750HP', total: 4, owned: 4 },
+  { project: '伊拉克钻修项目', model: '1000HP', total: 2, owned: 2 },
+  { project: '伊拉克钻修项目', model: '70D', total: 1, owned: 1 },
+  { project: '伊拉克一体化钻井项目', model: '70D', total: 1, owned: 0 },
+  { project: '阿根廷项目', model: '350HP', total: 3, owned: 3 },
+  { project: '埃塞项目', model: '50D', total: 2, owned: 0 }
+]
+
+function formatProjectName(value: string) {
+  return value.replace(/项目$/, '')
+}
+
+const categoryRows = computed(() =>
+  categoryData.map((item, index) => {
+    const nonOwned = Math.max(item.total - item.owned, 0)
+    const displayProject = formatProjectName(item.project)
+    const isFirstProjectRow = index === 0 || categoryData[index - 1].project !== item.project
+
+    return {
+      ...item,
+      displayProject,
+      projectLabel: isFirstProjectRow ? displayProject : '',
+      nonOwned
+    }
+  })
+)
+
+function getCategoryChartLayout() {
+  const { clientWidth = 0, clientHeight = 0 } = categoryChartRef.value ?? {}
+  const compact = clientHeight > 0 && (clientHeight < 210 || clientWidth < 520)
+
+  return {
+    compact,
+    gridTop: compact ? 26 : 34,
+    gridRight: compact ? 24 : 92,
+    gridBottom: compact ? 20 : 24,
+    gridLeft: compact ? 154 : 200,
+    legendLeft: compact ? 282 : 360,
+    legendItemSize: compact ? 8 : 10,
+    legendGap: compact ? 10 : 14,
+    axisFontSize: compact ? 10 : 12,
+    projectFontSize: compact ? 11 : 13,
+    modelLabelWidth: compact ? 44 : 54,
+    projectLabelWidth: compact ? 90 : 104,
+    projectOffset: compact ? 58 : 70,
+    barWidth: compact ? 9 : 13,
+    summaryLabelDistance: compact ? 4 : 6,
+    summaryLabelFontSize: compact ? 10 : 12
+  }
+}
+
+function getCategoryOption(data: any): echarts.EChartsOption {
+  const modelLabels = data.map((item) => item.model)
+  const projectLabels = data.map((item) => item.projectLabel)
+  const totalData = data.map((item) => item.total)
+  const ownedData = data.map((item) => item.owned)
+  const nonOwnedData = data.map((item) => item.nonOwned)
+  const maxTotal = Math.max(...totalData, 1)
+  const layout = getCategoryChartLayout()
+  const summaryLabel = {
+    show: true,
+    position: 'right' as any,
+    distance: layout.summaryLabelDistance,
+    color: THEME.text.strong,
+    fontSize: layout.summaryLabelFontSize,
+    fontWeight: 700,
+    fontFamily: FONT_FAMILY,
+    formatter(params: any) {
+      const row = data[params.dataIndex]
+
+      if (!row) return ''
+
+      return `${row.owned} / ${row.nonOwned}`
+    }
+  }
+
+  return {
+    ...ANIMATION,
+    grid: {
+      ...THEME.grid,
+      top: layout.gridTop,
+      right: layout.gridRight,
+      bottom: layout.gridBottom,
+      left: layout.gridLeft,
+      containLabel: false
+    },
+    color: [THEME.color.green.line, THEME.color.orange.line],
+    legend: createLegend(
+      {
+        top: 0,
+        right: 0,
+        left: layout.legendLeft,
+        itemWidth: layout.legendItemSize,
+        itemHeight: layout.legendItemSize,
+        itemGap: layout.legendGap,
+        selectedMode: true
+      },
+      ['自有设备', '非自有设备']
+    ),
+    tooltip: createTooltip({
+      trigger: 'axis',
+      axisPointer: {
+        type: 'shadow',
+        shadowStyle: {
+          color: THEME.split
+        }
+      },
+      formatter(params: any[]) {
+        const first = params[0]
+        const row = data[first?.dataIndex ?? 0]
+        if (!row) return ''
+
+        const ownedMarker =
+          '<span style="display:inline-block;width:8px;height:8px;margin-right:6px;border-radius:2px;background:#2e9a5b;"></span>'
+        const nonOwnedMarker =
+          '<span style="display:inline-block;width:8px;height:8px;margin-right:6px;border-radius:2px;background:#f08c2e;"></span>'
+
+        return [
+          `<div style="min-width:132px;font-weight:700;color:#24364f;margin-bottom:6px;">${row.displayProject} / ${row.model}</div>`,
+          `<div style="display:flex;justify-content:space-between;gap:18px;line-height:22px;"><span>${ownedMarker}自有设备</span><b style="color:#2e9a5b;">${row.owned}</b></div>`,
+          `<div style="display:flex;justify-content:space-between;gap:18px;line-height:22px;"><span>${nonOwnedMarker}非自有设备</span><b style="color:#f08c2e;">${row.nonOwned}</b></div>`,
+          `<div style="margin-top:4px;padding-top:4px;border-top:1px solid rgba(31,91,184,.12);display:flex;justify-content:space-between;gap:18px;line-height:22px;"><span>设备合计</span><b style="color:#1f5bb8;">${row.total}</b></div>`
+        ].join('')
+      }
+    }),
+    xAxis: {
+      type: 'value',
+      max: Math.max(10, Math.ceil(maxTotal + 1)),
+      axisLine: {
+        show: true
+      },
+      axisTick: {
+        show: true
+      },
+      axisLabel: {
+        show: true,
+        color: THEME.text.regular,
+        fontSize: layout.axisFontSize,
+        fontWeight: 500,
+        fontFamily: FONT_FAMILY
+      },
+      splitLine: {
+        lineStyle: {
+          color: THEME.split,
+          type: 'dashed'
+        }
+      }
+    },
+    yAxis: [
+      {
+        type: 'category',
+        data: modelLabels,
+        inverse: true,
+        axisLine: {
+          show: false
+        },
+        axisTick: {
+          show: false
+        },
+        axisLabel: {
+          color: THEME.text.regular,
+          fontSize: layout.axisFontSize,
+          fontWeight: 500,
+          fontFamily: FONT_FAMILY,
+          margin: layout.compact ? 6 : 10,
+          width: layout.modelLabelWidth,
+          overflow: 'truncate',
+          align: 'right'
+        }
+      },
+      {
+        type: 'category',
+        data: projectLabels,
+        inverse: true,
+        position: 'left',
+        offset: layout.projectOffset,
+        axisLine: {
+          show: false
+        },
+        axisTick: {
+          show: false
+        },
+        axisLabel: {
+          color: THEME.text.strong,
+          fontSize: layout.projectFontSize,
+          fontWeight: 700,
+          fontFamily: FONT_FAMILY,
+          margin: layout.compact ? 6 : 10,
+          width: layout.projectLabelWidth,
+          overflow: 'break',
+          align: 'right'
+        }
+      }
+    ],
+    series: [
+      {
+        name: '设备数量',
+        type: 'bar',
+        data: totalData,
+        barWidth: layout.barWidth,
+        silent: true,
+        z: 1,
+        label: summaryLabel,
+        labelLayout: {
+          hideOverlap: false
+        },
+        itemStyle: {
+          borderRadius: 999,
+          color: THEME.split
+        },
+        tooltip: {
+          show: false
+        }
+      },
+      {
+        name: '自有设备',
+        type: 'bar',
+        stack: 'device',
+        data: ownedData,
+        barWidth: layout.barWidth,
+        barGap: '-100%',
+        z: 3,
+        itemStyle: {
+          // borderRadius: [999, 0, 0, 999],
+          color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
+            { offset: 0, color: THEME.color.green.light },
+            { offset: 0.55, color: THEME.color.green.mid },
+            { offset: 1, color: THEME.color.green.line }
+          ])
+        }
+      },
+      {
+        name: '非自有设备',
+        type: 'bar',
+        stack: 'device',
+        data: nonOwnedData,
+        barWidth: layout.barWidth,
+        z: 3,
+        itemStyle: {
+          // borderRadius: [0, 999, 999, 0],
+          color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
+            { offset: 0, color: THEME.color.orange.light },
+            { offset: 0.55, color: THEME.color.orange.mid },
+            { offset: 1, color: THEME.color.orange.line }
+          ])
+        }
+      }
+    ]
+  }
+}
+
+function initCategoryChart() {
+  if (!categoryChartRef.value) return
+
+  if (categoryChart) {
+    categoryChart.dispose()
+  }
+
+  categoryChart = echarts.init(categoryChartRef.value, undefined, {
+    renderer: CHART_RENDERER
+  })
+  renderCategoryChart()
+}
+
+function renderCategoryChart() {
+  categoryChart?.setOption(getCategoryOption(categoryRows.value), true)
+}
+
+function resizeCategoryChart() {
+  categoryChart?.resize()
+  renderCategoryChart()
+}
+
+function destroyCategoryChart() {
+  if (categoryChart) {
+    categoryChart.dispose()
+    categoryChart = null
+  }
+}
+
+watch(activePanel, (value) => {
+  if (value !== 'category') return
+
+  nextTick(() => {
+    resizeCategoryChart()
+  })
+})
+
+onMounted(() => {
+  initCategoryChart()
+  window.addEventListener('resize', resizeCategoryChart)
+  window.addEventListener('rykb:resize', resizeCategoryChart)
+})
+
+onUnmounted(() => {
+  window.removeEventListener('resize', resizeCategoryChart)
+  window.removeEventListener('rykb:resize', resizeCategoryChart)
+  destroyCategoryChart()
+})
+</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>
+        {{ activeTitle }}
+      </div>
+      <el-segmented
+        v-model="activePanel"
+        :options="panelOptions"
+        size="small"
+        class="equipment-category-switch" />
+    </div>
+    <div class="flex-1 min-h-0">
+      <div v-show="activePanel === 'category'" ref="categoryChartRef" class="category-chart"></div>
+      <div v-show="activePanel === 'value'" class="h-full"></div>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+@import url('@/styles/kb.scss');
+
+.equipment-category-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(8px * var(--kb-scale, 1));
+    font-size: calc(13px * var(--kb-scale, 1));
+    font-weight: 600;
+    color: #29527f;
+  }
+}
+
+.category-chart {
+  width: 100%;
+  height: 100%;
+  min-height: 0;
+}
+</style>

+ 318 - 0
src/views/pms/stat/rykb/exception-prompt.vue

@@ -0,0 +1,318 @@
+<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
+  teamCount: number
+}
+
+const activeTimeRange = ref<TimeRange>('day')
+const loading = ref(false)
+const productionList = ref<NptCountItem[]>([])
+
+const timeOptions: Array<{ label: string; value: TimeRange }> = [
+  { label: '本日', value: 'day' },
+  { label: '本月', value: 'month' },
+  { label: '本季', value: 'quarter' },
+  { label: '全年', value: 'year' }
+]
+
+const productionTopList = computed(() => productionList.value.slice(0, 9))
+const deviceExceptionCount = ref(0)
+
+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')]
+}
+
+async function loadData() {
+  loading.value = true
+  try {
+    const res = await IotStatApi.getRyNptCount({
+      createTime: getCreateTime(activeTimeRange.value)
+    })
+
+    productionList.value = Array.isArray(res)
+      ? res.map((item) => ({
+          nptName: 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">
+            <span class="production-item__rank">{{ index + 1 }}</span>
+            <span class="production-item__name">{{ item.nptName }}</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>

+ 347 - 0
src/views/pms/stat/rykb/historical-workload.vue

@@ -0,0 +1,347 @@
+<script lang="ts" setup>
+import * as echarts from 'echarts'
+import {
+  ANIMATION,
+  ChartData,
+  createLegend,
+  createTooltip,
+  FONT_FAMILY,
+  formatSeriesName,
+  THEME
+} from '@/utils/kb'
+import { IotStatApi } from '@/api/pms/stat'
+
+type ActivePanel = 'zj' | 'xj'
+
+const activePanel = ref<ActivePanel>('zj')
+const chartData = ref<ChartData>({
+  xAxis: [],
+  series: []
+})
+const chartRef = ref<HTMLDivElement>()
+
+let chart: echarts.ECharts | null = null
+
+const panelOptions: Array<{ label: string; value: ActivePanel }> = [
+  {
+    label: '钻井',
+    value: 'zj'
+  },
+  {
+    label: '修井',
+    value: 'xj'
+  }
+]
+
+const activeTitle = computed(() =>
+  activePanel.value === 'zj' ? '钻井历史工作量' : '修井历史工作量'
+)
+
+function getBarStyle(color: (typeof THEME.color)[keyof typeof THEME.color]) {
+  return {
+    borderRadius: [12, 12, 0, 0],
+    shadowBlur: 12,
+    shadowColor: color.bg,
+    color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+      { offset: 0, color: color.light },
+      { offset: 0.55, color: color.mid },
+      { offset: 1, color: color.line }
+    ])
+  }
+}
+
+function formatValue(value: number | string) {
+  return Number(value || 0).toLocaleString('en-US')
+}
+
+function isFootageSeries(name: string) {
+  const displayName = formatSeriesName(name)
+
+  return name.includes('footage') || displayName.includes('进尺')
+}
+
+function isFinishedWellsSeries(name: string) {
+  const displayName = formatSeriesName(name)
+
+  return name.includes('finishedWells') || displayName.includes('完井')
+}
+
+function hasMixedUnits(data: ChartData) {
+  return (
+    data.series.some((item) => isFootageSeries(item.name)) &&
+    data.series.some((item) => isFinishedWellsSeries(item.name))
+  )
+}
+
+function getYAxisOption(data: ChartData): echarts.EChartsOption['yAxis'] {
+  const baseAxis = {
+    type: 'value' as const,
+    splitNumber: 4,
+    axisLine: {
+      show: false
+    },
+    axisTick: {
+      show: false
+    },
+    axisLabel: {
+      color: THEME.text.regular,
+      fontSize: 12,
+      fontFamily: FONT_FAMILY,
+      formatter(value: number) {
+        return formatValue(value)
+      }
+    },
+    splitLine: {
+      lineStyle: {
+        color: THEME.split,
+        type: 'dashed' as const
+      }
+    }
+  }
+
+  if (!hasMixedUnits(data)) {
+    return {
+      ...baseAxis,
+      name: data.series.some((item) => isFootageSeries(item.name))
+        ? '累计进尺(m)'
+        : '累计完井数(口)',
+      nameTextStyle: {
+        color: THEME.text.regular,
+        fontSize: 13,
+        fontFamily: FONT_FAMILY,
+        align: 'left'
+      }
+    }
+  }
+
+  return [
+    {
+      ...baseAxis,
+      name: '累计进尺(m)',
+      position: 'left',
+      nameTextStyle: {
+        color: THEME.text.regular,
+        fontSize: 13,
+        fontFamily: FONT_FAMILY,
+        align: 'left'
+      }
+    },
+    {
+      ...baseAxis,
+      name: '累计完井数(口)',
+      position: 'right',
+      nameTextStyle: {
+        color: THEME.text.regular,
+        fontSize: 13,
+        fontFamily: FONT_FAMILY,
+        align: 'right'
+      },
+      splitLine: {
+        show: false
+      }
+    }
+  ]
+}
+
+function getChartOption(data: ChartData): echarts.EChartsOption {
+  const xAxisData = data.xAxis || []
+  const seriesData = data.series || []
+  const colorList = [THEME.color.orange, THEME.color.blue, THEME.color.green]
+  const mixedUnits = hasMixedUnits(data)
+
+  return {
+    ...ANIMATION,
+    grid: {
+      ...THEME.grid,
+      top: 50,
+      right: mixedUnits ? 38 : THEME.grid.right,
+      bottom: 10
+    },
+    tooltip: createTooltip({
+      trigger: 'axis',
+      axisPointer: {
+        type: 'shadow',
+        shadowStyle: {
+          color: THEME.split
+        }
+      },
+      valueFormatter(value: number | string) {
+        return formatValue(value)
+      }
+    }),
+    legend: createLegend(
+      {
+        top: 4,
+        itemWidth: 12,
+        itemHeight: 12
+      },
+      seriesData.map((item) => formatSeriesName(item.name))
+    ),
+    xAxis: {
+      type: 'category',
+      data: xAxisData,
+      axisLine: {
+        show: false
+      },
+      axisTick: {
+        show: false
+      },
+      axisLabel: {
+        interval: 0,
+        color: THEME.text.regular,
+        fontSize: 14,
+        fontFamily: FONT_FAMILY
+      }
+    },
+    yAxis: getYAxisOption(data),
+    series: seriesData.map((item, index) => {
+      const color = colorList[index % colorList.length]
+      const useRightAxis = mixedUnits && isFinishedWellsSeries(item.name)
+
+      return {
+        name: formatSeriesName(item.name),
+        type: 'bar',
+        yAxisIndex: useRightAxis ? 1 : 0,
+        data: item.data || [],
+        barWidth: 24,
+        barMaxWidth: 32,
+        showBackground: true,
+        backgroundStyle: {
+          color: THEME.split,
+          borderRadius: [12, 12, 0, 0]
+        },
+        itemStyle: getBarStyle(color),
+        label: {
+          show: true,
+          position: 'top',
+          distance: 10,
+          color: color.strong,
+          fontSize: 14,
+          fontWeight: 700,
+          fontFamily: FONT_FAMILY,
+          formatter(params: any) {
+            return Number(params.value) ? formatValue(params.value) : ''
+          }
+        },
+        emphasis: {
+          focus: 'series',
+          itemStyle: {
+            ...getBarStyle(color),
+            shadowColor: color.shadow,
+            shadowBlur: 18
+          }
+        }
+      }
+    })
+  }
+}
+
+function initChart() {
+  if (!chartRef.value) return
+  chart?.dispose()
+  chart = echarts.init(chartRef.value, undefined, {
+    renderer: 'svg'
+  })
+  renderChart()
+}
+
+function renderChart() {
+  if (!chart) return
+  chart.setOption(getChartOption(chartData.value), true)
+}
+
+function resizeChart() {
+  chart?.resize()
+}
+
+function destroyChart() {
+  chart?.dispose()
+  chart = null
+}
+
+async function loadChart() {
+  try {
+    const res =
+      activePanel.value === 'zj'
+        ? await IotStatApi.getRyHiZjDailyReports()
+        : await IotStatApi.getRyHiXjDailyReports()
+
+    chartData.value = {
+      xAxis: (res?.xAxis || []).map((item) => `${item}`),
+      series: (res?.series || []).map((item) => ({
+        name: item.name,
+        data: item.data || []
+      }))
+    }
+    renderChart()
+  } catch (error) {
+    console.error(`获取${activeTitle.value}失败:`, error)
+    chartData.value = {
+      xAxis: [],
+      series: []
+    }
+    renderChart()
+  }
+}
+
+watch(activePanel, () => {
+  loadChart()
+})
+
+onMounted(() => {
+  initChart()
+  loadChart()
+  window.addEventListener('resize', resizeChart)
+  window.addEventListener('rykb:resize', resizeChart)
+})
+
+onUnmounted(() => {
+  window.removeEventListener('resize', resizeChart)
+  window.removeEventListener('rykb:resize', resizeChart)
+  destroyChart()
+})
+</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>
+        {{ activeTitle }}
+      </div>
+      <el-segmented
+        v-model="activePanel"
+        :options="panelOptions"
+        size="small"
+        class="historical-workload-switch" />
+    </div>
+    <div ref="chartRef" class="flex-1 min-h-0"></div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+@import url('@/styles/kb.scss');
+
+.historical-workload-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(8px * var(--kb-scale, 1));
+    font-size: calc(13px * var(--kb-scale, 1));
+    font-weight: 600;
+    color: #29527f;
+  }
+}
+</style>

+ 557 - 0
src/views/pms/stat/rykb/inventory-situation.vue

@@ -0,0 +1,557 @@
+<script lang="ts" setup>
+import * as echarts from 'echarts'
+import {
+  ANIMATION,
+  CHART_RENDERER,
+  createLegend,
+  createTooltip,
+  FONT_FAMILY,
+  THEME
+} from '@/utils/kb'
+
+type ActivePanel = 'distribution' | 'trend'
+
+type InventoryItem = {
+  project: string
+  yearBeginningAmount: number
+  mayInventoryAmount: number
+  increaseAmount: number
+  yearBeginningBacklog: number
+  mayBacklogAmount: number
+  backlogRatio: string
+}
+
+const activePanel = ref<ActivePanel>('distribution')
+const distributionChartRef = ref<HTMLDivElement>()
+const trendChartRef = ref<HTMLDivElement>()
+
+let distributionChart: echarts.ECharts | null = null
+let trendChart: echarts.ECharts | null = null
+
+const panelOptions: Array<{ label: string; value: ActivePanel }> = [
+  {
+    label: '分布',
+    value: 'distribution'
+  },
+  {
+    label: '趋势',
+    value: 'trend'
+  }
+]
+
+const activeTitle = computed(() => (activePanel.value === 'distribution' ? '存货分布' : '积压趋势'))
+
+const inventoryData: InventoryItem[] = [
+  {
+    project: 'SCP项目',
+    yearBeginningAmount: 119.08,
+    mayInventoryAmount: 123.63,
+    increaseAmount: 4.55,
+    yearBeginningBacklog: 23.63,
+    mayBacklogAmount: 43.07,
+    backlogRatio: '34.84%'
+  },
+  {
+    project: '紫金山项目',
+    yearBeginningAmount: 79.97,
+    mayInventoryAmount: 82.95,
+    increaseAmount: 2.98,
+    yearBeginningBacklog: 4.32,
+    mayBacklogAmount: 24.76,
+    backlogRatio: '29.85%'
+  },
+  {
+    project: '青海东台项目',
+    yearBeginningAmount: 10.42,
+    mayInventoryAmount: 10.32,
+    increaseAmount: -0.1,
+    yearBeginningBacklog: 10.42,
+    mayBacklogAmount: 10.32,
+    backlogRatio: '100.00%'
+  },
+  {
+    project: '新疆项目',
+    yearBeginningAmount: 156.11,
+    mayInventoryAmount: 159.45,
+    increaseAmount: 3.34,
+    yearBeginningBacklog: 28,
+    mayBacklogAmount: 26.84,
+    backlogRatio: '16.83%'
+  },
+  {
+    project: '泰安项目',
+    yearBeginningAmount: 13.93,
+    mayInventoryAmount: 13.62,
+    increaseAmount: -0.31,
+    yearBeginningBacklog: 5.18,
+    mayBacklogAmount: 6.31,
+    backlogRatio: '46.35%'
+  },
+  {
+    project: '伊拉克项目',
+    yearBeginningAmount: 1205.77,
+    mayInventoryAmount: 1098.32,
+    increaseAmount: -107.45,
+    yearBeginningBacklog: 227.67,
+    mayBacklogAmount: 350.58,
+    backlogRatio: '31.92%'
+  },
+  {
+    project: '伊拉克一体化项目',
+    yearBeginningAmount: 333.36,
+    mayInventoryAmount: 369.97,
+    increaseAmount: 36.61,
+    yearBeginningBacklog: 0,
+    mayBacklogAmount: 0,
+    backlogRatio: '0.00%'
+  },
+  {
+    project: '国外项目暂存物资',
+    yearBeginningAmount: 134.49,
+    mayInventoryAmount: 156.73,
+    increaseAmount: 22.24,
+    yearBeginningBacklog: 0,
+    mayBacklogAmount: 0,
+    backlogRatio: '0.00%'
+  }
+]
+
+function formatProjectName(value: string) {
+  return value.replace(/项目$/, '')
+}
+
+function formatAmount(value: number) {
+  return Number(value || 0).toFixed(2)
+}
+
+function getInventoryChartLayout(chartRef: Ref<HTMLDivElement | undefined>) {
+  const { clientWidth = 0, clientHeight = 0 } = chartRef.value ?? {}
+  const compact = clientHeight > 0 && (clientHeight < 210 || clientWidth < 520)
+
+  return {
+    compact,
+    trendGridTop: compact ? 24 : 32,
+    trendGridRight: compact ? 12 : THEME.grid.right,
+    trendGridBottom: compact ? 10 : THEME.grid.bottom,
+    legendTop: compact ? 0 : 4,
+    legendRight: compact ? 2 : 6,
+    legendItemSize: compact ? 9 : 12,
+    legendGap: compact ? 10 : 16,
+    legendFontSize: compact ? 11 : 14,
+    axisFontSize: compact ? 10 : 12,
+    yAxisLabelWidth: compact ? 96 : 132,
+    yAxisLabelMargin: compact ? 8 : 12,
+    distributionTitleTop: compact ? 6 : 18,
+    distributionTitleFontSize: compact ? 12 : 14,
+    distributionTitleLineHeight: compact ? 14 : 16,
+    distributionPieRadius: compact ? ['36%', '54%'] : ['48%', '68%'],
+    distributionPieCenterY: compact ? '62%' : '57%',
+    distributionLegendBottom: compact ? 0 : 10,
+    distributionLegendItemSize: compact ? 9 : 13,
+    distributionLegendGap: compact ? 8 : 14,
+    distributionLegendFontSize: compact ? 10 : 14,
+    pieLabelNameFontSize: compact ? 11 : 13,
+    pieLabelNameLineHeight: compact ? 18 : 22,
+    pieLabelValueFontSize: compact ? 16 : 20,
+    pieLabelValueLineHeight: compact ? 22 : 30,
+    barWidth: compact ? 8 : 12,
+    barGap: compact ? '12%' : '5%',
+    barCategoryGap: compact ? '46%' : '36%',
+    labelDistance: compact ? 4 : 7,
+    labelFontSize: compact ? 10 : 12
+  }
+}
+
+function getDistributionOption(data: InventoryItem[]): echarts.EChartsOption {
+  const layout = getInventoryChartLayout(distributionChartRef)
+  const inventoryTotal = data.reduce((total, item) => total + item.mayInventoryAmount, 0)
+  const backlogTotal = data.reduce((total, item) => total + item.mayBacklogAmount, 0)
+
+  return {
+    ...ANIMATION,
+    color: [
+      THEME.color.blue.line,
+      THEME.color.orange.line,
+      THEME.color.green.line,
+      THEME.color.red.line,
+      THEME.color.blue.mid,
+      THEME.color.orange.mid,
+      THEME.color.green.mid,
+      THEME.color.red.mid
+    ],
+    tooltip: createTooltip({
+      trigger: 'item',
+      formatter(params: any) {
+        return `${params.seriesName}<br/>${params.marker}${params.name}:${formatAmount(
+          params.value
+        )}万元<br/>占比:${params.percent}%`
+      }
+    }),
+    title: [
+      {
+        text: `5月底库存金额\n${formatAmount(inventoryTotal)} 万元`,
+        left: '26.5%',
+        top: layout.distributionTitleTop,
+        textAlign: 'center',
+        textStyle: {
+          color: THEME.text.strong,
+          fontSize: layout.distributionTitleFontSize,
+          fontWeight: 700,
+          lineHeight: layout.distributionTitleLineHeight,
+          fontFamily: FONT_FAMILY
+        }
+      },
+      {
+        text: `5月底积压库存金额\n${formatAmount(backlogTotal)} 万元`,
+        left: '72.5%',
+        top: layout.distributionTitleTop,
+        textAlign: 'center',
+        textStyle: {
+          color: THEME.text.strong,
+          fontSize: layout.distributionTitleFontSize,
+          fontWeight: 700,
+          lineHeight: layout.distributionTitleLineHeight,
+          fontFamily: FONT_FAMILY
+        }
+      }
+    ],
+    legend: createLegend({
+      type: 'scroll',
+      bottom: layout.distributionLegendBottom,
+      left: 10,
+      right: 10,
+      itemWidth: layout.distributionLegendItemSize,
+      itemHeight: layout.distributionLegendItemSize,
+      itemGap: layout.distributionLegendGap,
+      textStyle: {
+        color: THEME.text.regular,
+        fontSize: layout.distributionLegendFontSize,
+        fontWeight: 600,
+        fontFamily: FONT_FAMILY
+      }
+    }),
+    series: [
+      {
+        name: '5月底库存金额',
+        type: 'pie',
+        radius: layout.distributionPieRadius,
+        center: ['27%', layout.distributionPieCenterY],
+        minAngle: 5,
+        data: data.map((item) => ({
+          name: item.project,
+          value: item.mayInventoryAmount
+        }))
+      },
+      {
+        name: '5月底积压',
+        type: 'pie',
+        radius: layout.distributionPieRadius,
+        center: ['73%', layout.distributionPieCenterY],
+        minAngle: 5,
+        data: data
+          .filter((item) => item.mayBacklogAmount > 0)
+          .map((item) => ({
+            name: item.project,
+            value: item.mayBacklogAmount
+          }))
+      }
+    ]
+  }
+}
+
+function getTrendOption(data: InventoryItem[]): echarts.EChartsOption {
+  const projects = data.map((item) => formatProjectName(item.project))
+  const maxBacklog = Math.max(
+    ...data.map((item) => Math.max(item.yearBeginningBacklog, item.mayBacklogAmount)),
+    1
+  )
+  const backlogAxisMax = Math.ceil((maxBacklog * 1.15) / 50) * 50
+  const layout = getInventoryChartLayout(trendChartRef)
+  const barLabel = {
+    show: true,
+    position: 'right' as any,
+    distance: layout.labelDistance,
+    color: THEME.text.strong,
+    fontSize: layout.labelFontSize,
+    fontWeight: 700,
+    fontFamily: FONT_FAMILY,
+    formatter(params: any) {
+      const value = Number(params.value)
+
+      return formatAmount(value)
+    }
+  }
+
+  return {
+    ...ANIMATION,
+    grid: {
+      ...THEME.grid,
+      top: layout.trendGridTop,
+      right: layout.trendGridRight,
+      bottom: layout.trendGridBottom
+    },
+    color: [THEME.color.blue.line, THEME.color.orange.line],
+    legend: createLegend(
+      {
+        top: layout.legendTop,
+        right: layout.legendRight,
+        itemWidth: layout.legendItemSize,
+        itemHeight: layout.legendItemSize,
+        itemGap: layout.legendGap,
+        textStyle: {
+          color: THEME.text.regular,
+          fontSize: layout.legendFontSize,
+          fontWeight: 600,
+          fontFamily: FONT_FAMILY
+        }
+      },
+      ['年初积压库存', '5月底积压库存']
+    ),
+    tooltip: createTooltip({
+      trigger: 'axis',
+      axisPointer: {
+        type: 'shadow',
+        shadowStyle: {
+          color: THEME.split
+        }
+      },
+      valueFormatter(value: number) {
+        return `${formatAmount(value)}万元`
+      }
+    }),
+    xAxis: {
+      type: 'value',
+      max: backlogAxisMax,
+      splitNumber: 4,
+      axisLine: {
+        show: false
+      },
+      axisTick: {
+        show: false
+      },
+      axisLabel: {
+        color: THEME.text.regular,
+        fontSize: layout.axisFontSize,
+        fontFamily: FONT_FAMILY,
+        formatter(value: number) {
+          return `${value}`
+        }
+      },
+      splitLine: {
+        lineStyle: {
+          color: THEME.split,
+          type: 'dashed'
+        }
+      }
+    },
+    yAxis: {
+      type: 'category',
+      data: projects,
+      inverse: true,
+      axisLine: {
+        show: false
+      },
+      axisTick: {
+        show: false
+      },
+      axisLabel: {
+        color: THEME.text.regular,
+        fontSize: layout.axisFontSize,
+        fontWeight: 600,
+        fontFamily: FONT_FAMILY,
+        margin: layout.yAxisLabelMargin,
+        width: layout.yAxisLabelWidth,
+        overflow: 'break',
+        align: 'right'
+      }
+    },
+    series: [
+      {
+        name: '年初积压库存',
+        type: 'bar',
+        data: data.map((item) => item.yearBeginningBacklog),
+        barWidth: layout.barWidth,
+        barGap: layout.barGap,
+        barCategoryGap: layout.barCategoryGap,
+        barMinHeight: 0,
+        showBackground: false,
+        backgroundStyle: {
+          color: THEME.split,
+          borderRadius: 999
+        },
+        label: barLabel,
+        labelLayout: {
+          hideOverlap: false
+        },
+        itemStyle: {
+          shadowBlur: 10,
+          shadowColor: THEME.color.blue.bg,
+          color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
+            { offset: 0, color: THEME.color.blue.light },
+            { offset: 0.55, color: THEME.color.blue.mid },
+            { offset: 1, color: THEME.color.blue.line }
+          ])
+        }
+      },
+      {
+        name: '5月底积压库存',
+        type: 'bar',
+        data: data.map((item) => item.mayBacklogAmount),
+        barWidth: layout.barWidth,
+        barMinHeight: 0,
+        barGap: layout.barGap,
+        barCategoryGap: layout.barCategoryGap,
+        showBackground: false,
+        backgroundStyle: {
+          color: THEME.split,
+          borderRadius: 999
+        },
+        label: barLabel,
+        labelLayout: {
+          hideOverlap: false
+        },
+        itemStyle: {
+          shadowBlur: 10,
+          shadowColor: THEME.color.orange.bg,
+          color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
+            { offset: 0, color: THEME.color.orange.light },
+            { offset: 0.55, color: THEME.color.orange.mid },
+            { offset: 1, color: THEME.color.orange.line }
+          ])
+        }
+      }
+    ]
+  }
+}
+
+function initChart(
+  chartRef: Ref<HTMLDivElement | undefined>,
+  chart: echarts.ECharts | null,
+  option: echarts.EChartsOption
+) {
+  if (!chartRef.value) return chart
+
+  chart?.dispose()
+  const nextChart = echarts.init(chartRef.value, undefined, {
+    renderer: CHART_RENDERER
+  })
+  nextChart.setOption(option, true)
+
+  return nextChart
+}
+
+function renderDistributionChart() {
+  distributionChart?.setOption(getDistributionOption(inventoryData), true)
+}
+
+function renderTrendChart() {
+  trendChart?.setOption(getTrendOption(inventoryData), true)
+}
+
+function initDistributionChart() {
+  distributionChart = initChart(
+    distributionChartRef,
+    distributionChart,
+    getDistributionOption(inventoryData)
+  )
+}
+
+function initTrendChart() {
+  trendChart = initChart(trendChartRef, trendChart, getTrendOption(inventoryData))
+}
+
+function resizeCharts() {
+  distributionChart?.resize()
+  trendChart?.resize()
+  renderDistributionChart()
+  renderTrendChart()
+}
+
+function destroyCharts() {
+  distributionChart?.dispose()
+  trendChart?.dispose()
+  distributionChart = null
+  trendChart = null
+}
+
+watch(activePanel, (value) => {
+  nextTick(() => {
+    if (value === 'distribution') {
+      if (!distributionChart) initDistributionChart()
+      renderDistributionChart()
+    } else {
+      if (!trendChart) initTrendChart()
+      renderTrendChart()
+    }
+    resizeCharts()
+  })
+})
+
+onMounted(() => {
+  initDistributionChart()
+  window.addEventListener('resize', resizeCharts)
+  window.addEventListener('rykb:resize', resizeCharts)
+})
+
+onUnmounted(() => {
+  window.removeEventListener('resize', resizeCharts)
+  window.removeEventListener('rykb:resize', resizeCharts)
+  destroyCharts()
+})
+</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>
+        {{ activeTitle }}
+      </div>
+      <el-segmented
+        v-model="activePanel"
+        :options="panelOptions"
+        size="small"
+        class="inventory-switch" />
+    </div>
+    <div class="flex-1 min-h-0">
+      <div
+        v-show="activePanel === 'distribution'"
+        ref="distributionChartRef"
+        class="inventory-chart"></div>
+      <div v-show="activePanel === 'trend'" ref="trendChartRef" class="inventory-chart"></div>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+@import url('@/styles/kb.scss');
+
+.inventory-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(8px * var(--kb-scale, 1));
+    font-size: calc(13px * var(--kb-scale, 1));
+    font-weight: 600;
+    color: #29527f;
+  }
+}
+
+.inventory-chart {
+  width: 100%;
+  height: 100%;
+  min-height: 0;
+}
+</style>

+ 86 - 48
src/views/pms/stat/rykb/rysummary.vue

@@ -14,10 +14,32 @@ type CardKey =
   | 'inspecttfinished'
   | 'zj'
   | 'xj'
+  | 'utilizationRate'
 
 type CardConfig = SummaryCardConfig<CardKey>
 
 const cardConfigs: CardConfig[] = [
+  {
+    key: 'zj',
+    title: '钻井总进尺(m)',
+    icon: 'i-solar:ruler-bold',
+    accent: THEME.color.green.strong,
+    glow: THEME.color.green.glow
+  },
+  {
+    key: 'xj',
+    title: '修井总完成井数',
+    icon: 'i-mdi:wrench-check-outline',
+    accent: THEME.color.green.strong,
+    glow: THEME.color.green.glow
+  },
+  {
+    key: 'utilizationRate',
+    title: '累计设备利用率(%)',
+    icon: 'i-material-symbols:device-hub-rounded',
+    accent: THEME.color.green.strong,
+    glow: THEME.color.green.glow
+  },
   {
     key: 'device',
     title: '设备数',
@@ -39,27 +61,13 @@ const cardConfigs: CardConfig[] = [
     accent: THEME.color.orange.strong,
     glow: THEME.color.orange.glow
   },
-  {
-    key: 'filledCount',
-    title: '运行已填写',
-    icon: 'i-solar:clipboard-check-linear',
-    accent: THEME.color.green.strong,
-    glow: THEME.color.green.glow
-  },
-  {
-    key: 'bytodo',
-    title: '待保养',
-    icon: 'i-solar:shield-warning-linear',
-    accent: THEME.color.orange.strong,
-    glow: THEME.color.orange.glow
-  },
-  {
-    key: 'byfinished',
-    title: '已保养',
-    icon: 'i-solar:shield-check-linear',
-    accent: THEME.color.green.strong,
-    glow: THEME.color.green.glow
-  },
+  // {
+  //   key: 'filledCount',
+  //   title: '运行已填写',
+  //   icon: 'i-solar:clipboard-check-linear',
+  //   accent: THEME.color.green.strong,
+  //   glow: THEME.color.green.glow
+  // },
   {
     key: 'inspectttodo',
     title: '待巡检',
@@ -68,26 +76,26 @@ const cardConfigs: CardConfig[] = [
     glow: THEME.color.orange.glow
   },
   {
-    key: 'inspecttfinished',
-    title: '已巡检',
-    icon: 'i-solar:check-circle-linear',
-    accent: THEME.color.green.strong,
-    glow: THEME.color.green.glow
-  },
-  {
-    key: 'zj',
-    title: '钻井总进尺(m)',
-    icon: 'i-solar:ruler-bold',
-    accent: THEME.color.green.strong,
-    glow: THEME.color.green.glow
-  },
-  {
-    key: 'xj',
-    title: '修井总完成井数',
-    icon: 'i-mdi:wrench-check-outline',
-    accent: THEME.color.green.strong,
-    glow: THEME.color.green.glow
+    key: 'bytodo',
+    title: '待保养',
+    icon: 'i-solar:shield-warning-linear',
+    accent: THEME.color.orange.strong,
+    glow: THEME.color.orange.glow
   }
+  // {
+  //   key: 'byfinished',
+  //   title: '已保养',
+  //   icon: 'i-solar:shield-check-linear',
+  //   accent: THEME.color.green.strong,
+  //   glow: THEME.color.green.glow
+  // },
+  // {
+  //   key: 'inspecttfinished',
+  //   title: '已巡检',
+  //   icon: 'i-solar:check-circle-linear',
+  //   accent: THEME.color.green.strong,
+  //   glow: THEME.color.green.glow
+  // },
 ]
 
 function createDefaultCardState(): Record<CardKey, CardStateItem> {
@@ -101,7 +109,8 @@ function createDefaultCardState(): Record<CardKey, CardStateItem> {
     inspectttodo: { value: 0, loading: true },
     inspecttfinished: { value: 0, loading: true },
     zj: { value: 0, loading: true },
-    xj: { value: 0, loading: true }
+    xj: { value: 0, loading: true },
+    utilizationRate: { value: 0, loading: true }
   }
 }
 
@@ -130,6 +139,13 @@ function setCardLoading(keys: CardKey[], loading: boolean) {
   })
 }
 
+function getCurrentYearCreateTime() {
+  return [
+    dayjs().startOf('year').format('YYYY-MM-DD HH:mm:ss'),
+    dayjs().endOf('year').format('YYYY-MM-DD HH:mm:ss')
+  ]
+}
+
 async function loadDeviceCard() {
   const keys: CardKey[] = ['device']
   setCardLoading(keys, true)
@@ -232,8 +248,12 @@ async function loadZjXj() {
   const keys: CardKey[] = ['zj', 'xj']
   setCardLoading(keys, true)
 
+  const params = {
+    createTime: getCurrentYearCreateTime()
+  }
+
   try {
-    const res = await IotStatApi.getInspectZjxjCount()
+    const res = await IotStatApi.getInspectZjxjCount(params)
     setCardValue('zj', res?.zj)
     setCardValue('xj', res?.xj)
   } catch (error) {
@@ -245,6 +265,25 @@ async function loadZjXj() {
   }
 }
 
+async function loadRate() {
+  const keys: CardKey[] = ['utilizationRate']
+  setCardLoading(keys, true)
+
+  const params = {
+    createTime: getCurrentYearCreateTime()
+  }
+
+  try {
+    const res = await IotStatApi.getInspectRate(params)
+    setCardValue('utilizationRate', (res * 100).toFixed(2))
+  } catch (error) {
+    console.error('获取累计设备利用率失败:', error)
+    setCardValue('utilizationRate', 0)
+  } finally {
+    setCardLoading(keys, false)
+  }
+}
+
 function loadAllCards() {
   loadDeviceCard()
   loadMaintainCard()
@@ -252,6 +291,7 @@ function loadAllCards() {
   loadMaintainStatusCards()
   loadInspectCards()
   loadZjXj()
+  loadRate()
 }
 
 onMounted(() => {
@@ -266,10 +306,10 @@ onMounted(() => {
         <span></span>
         <span></span>
       </div>
-      工单情
+      运行概
     </div>
 
-    <div class="summary-panel__grid grid grid-cols-10 flex-1">
+    <div class="summary-panel__grid grid grid-cols-8 flex-1">
       <article
         v-for="card in summaryCards"
         :key="card.key"
@@ -277,8 +317,7 @@ onMounted(() => {
         :style="{
           '--card-accent': card.accent,
           '--card-glow': card.glow
-        }"
-      >
+        }">
         <div class="summary-card__shine"></div>
 
         <div class="summary-card__icon">
@@ -293,8 +332,7 @@ onMounted(() => {
               style="color: #1f5bb8"
               :start-val="0"
               :end-val="card.value"
-              :duration="1200"
-            />
+              :duration="1200" />
             <span v-else class="summary-card__placeholder">--</span>
           </div>
         </div>

+ 1 - 1
src/views/pms/stat/rykb/safeday.vue

@@ -8,7 +8,7 @@ const safeDays = ref(FIXED_SAFE_DAYS)
 
 async function loadSafeDays() {
   try {
-    const res = await IotStatApi.getSafeCount1()
+    const res = await IotStatApi.getSafeCount1(158)
     safeDays.value = res
   } catch (error) {
     console.error('获取安全生产天数失败:', error)

+ 2 - 1
src/views/pms/stat/rykb/zjfinish.vue

@@ -89,7 +89,8 @@ function getChartOption(data: ChartData): echarts.EChartsOption {
       nameTextStyle: {
         color: THEME.text.regular,
         fontSize: 13,
-        fontFamily: FONT_FAMILY
+        fontFamily: FONT_FAMILY,
+        align: 'left'
       },
       splitNumber: 4,
       axisLine: {

+ 2 - 1
src/views/pms/stat/rykb/zjwork.vue

@@ -98,7 +98,8 @@ function getChartOption(data: ChartData): echarts.EChartsOption {
         nameTextStyle: {
           color: THEME.text.regular,
           fontSize: 13,
-          fontFamily: FONT_FAMILY
+          fontFamily: FONT_FAMILY,
+          align: 'left'
         },
         splitNumber: 4,
         axisLine: {

+ 50 - 31
src/views/report-statistics/costs.vue

@@ -26,10 +26,30 @@ const timeOptions: { label: string; value: TimeType }[] = [
   { label: '日', value: 'day' }
 ]
 
-const activeTimeType = ref<TimeType | undefined>('year')
+const route = useRoute()
+
+const getRouteCreateTime = (): [string, string] | undefined => {
+  const createTime = route.query.createTime
+
+  if (Array.isArray(createTime)) {
+    const values = createTime.filter((item): item is string => typeof item === 'string')
+    return values.length === 2 ? [values[0], values[1]] : undefined
+  }
+
+  if (typeof createTime === 'string') {
+    const values = createTime.split(',').filter(Boolean)
+    return values.length === 2 ? [values[0], values[1]] : undefined
+  }
+
+  return undefined
+}
+
+const routeCreateTime = getRouteCreateTime()
+const activeTimeType = ref<TimeType | undefined>(routeCreateTime ? undefined : 'year')
 const query = ref<Query>({
   pageNo: 1,
-  pageSize: 10
+  pageSize: 10,
+  createTime: routeCreateTime
 })
 
 const handleTimeChange = (type: TimeType, init = false) => {
@@ -183,7 +203,9 @@ const loadList = useDebounceFn(async function () {
 }, 500)
 
 onMounted(() => {
-  handleTimeChange('year', true)
+  if (!query.value.createTime) {
+    handleTimeChange('year', true)
+  }
 })
 
 watch(
@@ -194,6 +216,17 @@ watch(
   { immediate: true }
 )
 
+watch(
+  () => route.query.createTime,
+  () => {
+    const createTime = getRouteCreateTime()
+    if (!createTime) return
+
+    activeTimeType.value = undefined
+    query.value.createTime = createTime
+  }
+)
+
 function selectType(type: string | undefined) {
   query.value.type = type
   query.value.pageNo = 1
@@ -229,16 +262,14 @@ const handleChange = () => {
 
 <template>
   <div
-    class="grid grid-cols-[auto_1fr] grid-rows-[196px_1fr] gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]"
-  >
+    class="grid grid-cols-[auto_1fr] grid-rows-[196px_1fr] gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]">
     <DeptTreeSelect
       :top-id="156"
       :deptId="156"
       v-model="query.deptId"
       :init-select="false"
       :show-title="false"
-      class="row-span-2"
-    />
+      class="row-span-2" />
     <!-- <div class="p-4 bg-white dark:bg-[#1d1e1f] shadow rounded-lg row-span-2"> </div> -->
     <div class="grid grid-rows-[1fr_32px] gap-4">
       <div class="grid grid-cols-3 gap-4" v-loading="dataLoading">
@@ -248,8 +279,7 @@ const handleChange = () => {
           :key="item.key"
           class="flex flex-col items-center gap-y-4 rounded-lg shadow p-4 transition-transform hover:scale-105 duration-500"
           :class="{ [item.class.bg1]: true, 'scale-105': item.type === query.type }"
-          @click="selectType(item.type)"
-        >
+          @click="selectType(item.type)">
           <!-- 头部:图标 + 标题 -->
           <div class="flex items-center gap-x-3">
             <div class="rounded-2 p-2" :class="[item.class.text, item.class.bg2]">
@@ -267,8 +297,7 @@ const handleChange = () => {
             :end-val="item.value"
             :decimals="2"
             suffix="元"
-            :duration="1000"
-          >
+            :duration="1000">
             <!-- 插槽内容:当数据为空或0时的显示 (根据 count-to 组件的具体实现决定是否显示) -->
             <span class="text-xs leading-8 text-[var(--el-text-color-regular)]">暂无数据</span>
           </count-to>
@@ -279,16 +308,14 @@ const handleChange = () => {
             <div
               v-show="item.hb"
               class="flex items-center gap-x-1 text-xs font-medium"
-              :class="item.class.text"
-            >
+              :class="item.class.text">
               <!-- 动态图标:大于等于0向上,小于0向下 -->
               <div
                 :class="
                   item.hb >= 0
                     ? 'i-material-symbols:arrow-warm-up-rounded'
                     : 'i-material-symbols:arrow-cool-down-rounded'
-                "
-              ></div>
+                "></div>
 
               <span class="vertical-middle"> {{ item.hb > 0 ? '+' + item.hb : item.hb }}% </span>
               <span>环比</span>
@@ -296,16 +323,14 @@ const handleChange = () => {
             <div
               v-show="item.tb"
               class="flex items-center gap-x-1 text-xs font-medium"
-              :class="item.class.text"
-            >
+              :class="item.class.text">
               <!-- 动态图标:大于等于0向上,小于0向下 -->
               <div
                 :class="
                   item.tb >= 0
                     ? 'i-material-symbols:arrow-warm-up-rounded'
                     : 'i-material-symbols:arrow-cool-down-rounded'
-                "
-              ></div>
+                "></div>
 
               <span class="vertical-middle"> {{ item.tb > 0 ? '+' + item.tb : item.tb }}% </span>
               <span>同比</span>
@@ -327,15 +352,13 @@ const handleChange = () => {
             @clear="handleClear"
             @change="handleChange"
             :clearable="false"
-            :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
-          />
+            :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" />
           <el-button-group size="default">
             <el-button
               v-for="item in timeOptions"
               :key="item.value"
               :type="activeTimeType === item.value ? 'primary' : ''"
-              @click="handleTimeChange(item.value)"
-            >
+              @click="handleTimeChange(item.value)">
               {{ item.label }}
             </el-button>
           </el-button-group>
@@ -347,8 +370,7 @@ const handleChange = () => {
             plain
             type="success"
             @click="handleExport"
-            :loading="exportLoading"
-          >
+            :loading="exportLoading">
             <Icon icon="ep:download" class="mr-5px" /> 导出
           </el-button>
         </div>
@@ -364,15 +386,13 @@ const handleChange = () => {
               :width="width"
               :height="height"
               :max-height="height"
-              show-border
-            >
+              show-border>
               <ZmTableColumn
                 label="序号"
                 type="index"
                 :width="70"
                 fixed="left"
-                hide-in-column-settings
-              />
+                hide-in-column-settings />
               <ZmTableColumn label="日期" prop="date" min-width="140" />
               <ZmTableColumn label="类别" prop="type" min-width="120" />
               <ZmTableColumn label="设备编号" prop="deviceCode" min-width="150" />
@@ -395,8 +415,7 @@ const handleChange = () => {
           :total="total"
           layout="total, sizes, prev, pager, next, jumper"
           @size-change="handleSizeChange"
-          @current-change="handleCurrentChange"
-        />
+          @current-change="handleCurrentChange" />
       </div>
     </div>
   </div>