Ver Fonte

隐患排查分类

yanghao há 21 horas atrás
pai
commit
21921a70e0
32 ficheiros alterados com 779 adições e 370 exclusões
  1. 2 1
      src/layout/components/Menu/src/components/useRenderMenuItem.tsx
  2. 138 48
      src/styles/kb.scss
  3. 41 19
      src/views/Home/Index.vue
  4. 5 3
      src/views/Home/kb/dayfinish.vue
  5. 3 1
      src/views/Home/kb/hdeviceStatus.vue
  6. 3 1
      src/views/Home/kb/hdeviceType.vue
  7. 14 13
      src/views/Home/kb/hmttr.vue
  8. 3 1
      src/views/Home/kb/horderTrend.vue
  9. 15 13
      src/views/Home/kb/hsafe.vue
  10. 4 4
      src/views/Home/kb/hsummary.vue
  11. 205 156
      src/views/pms/operation-meeting/components/operation-meeting-content.vue
  12. 30 17
      src/views/pms/stat/rhkb.vue
  13. 9 6
      src/views/pms/stat/rhkb/deviceList.vue
  14. 3 1
      src/views/pms/stat/rhkb/deviceStatus.vue
  15. 3 1
      src/views/pms/stat/rhkb/deviceType.vue
  16. 3 1
      src/views/pms/stat/rhkb/historyGas.vue
  17. 3 1
      src/views/pms/stat/rhkb/operation.vue
  18. 3 1
      src/views/pms/stat/rhkb/orderTrend.vue
  19. 4 4
      src/views/pms/stat/rhkb/rhsummary.vue
  20. 3 1
      src/views/pms/stat/rhkb/todayGas.vue
  21. 129 25
      src/views/pms/stat/rykb.vue
  22. 73 10
      src/views/pms/stat/rykb/ryProductionBriefs.vue
  23. 35 7
      src/views/pms/stat/rykb/rydeviceList.vue
  24. 3 1
      src/views/pms/stat/rykb/rydeviceStatus.vue
  25. 3 1
      src/views/pms/stat/rykb/rydeviceType.vue
  26. 3 1
      src/views/pms/stat/rykb/ryorderTrend.vue
  27. 4 4
      src/views/pms/stat/rykb/rysummary.vue
  28. 19 18
      src/views/pms/stat/rykb/safeday.vue
  29. 3 1
      src/views/pms/stat/rykb/xjwork.vue
  30. 7 7
      src/views/pms/stat/rykb/zjStatsSwitch.vue
  31. 3 1
      src/views/pms/stat/rykb/zjfinish.vue
  32. 3 1
      src/views/pms/stat/rykb/zjwork.vue

+ 2 - 1
src/layout/components/Menu/src/components/useRenderMenuItem.tsx

@@ -14,13 +14,14 @@ export const useRenderMenuItem = () =>
     const renderMenuItem = (routers: AppRouteRecordRaw[], parentPath = '/') => {
       return routers
         .filter((v) => {
+          console.log('v', v.path)
           if (currentSource === 'zhly') {
             return (
               (!v.meta?.hidden &&
                 ['智慧连油', '连油监控', '监控查询', '视频告警', '告警配置', '连油看板'].includes(
                   v.meta?.title
                 )) ||
-              (!v.meta?.hidden && ['/oli-connection/split_view1'].includes(v.path))
+              (!v.meta?.hidden && ['split_view1'].includes(v.path))
             )
           } else if (currentSource === 'znzq') {
             return !v.meta?.hidden && ['/device_monitor'].includes(v.path)

+ 138 - 48
src/styles/kb.scss

@@ -14,9 +14,9 @@
 }
 
 .header {
-  height: 52px;
+  height: calc(52px * var(--kb-scale, 1));
   font-family: YouSheBiaoTiHei, sans-serif;
-  font-size: 32px;
+  font-size: calc(32px * var(--kb-scale, 1));
   color: var(--fvs-theme-color-1-2);
   text-align: center;
   background: url('@/assets/kb/header.png') center center / 100% 100% no-repeat;
@@ -25,7 +25,7 @@
 .panel {
   background: linear-gradient(180deg, rgb(235 243 255 / 76%) 0%, rgb(221 233 251 / 58%) 100%);
   border: 1px solid rgb(255 255 255 / 58%);
-  border-radius: 20px;
+  border-radius: calc(20px * var(--kb-scale, 1));
   box-shadow:
     inset 0 1px 0 rgb(255 255 255 / 72%),
     0 18px 36px rgb(46 90 164 / 12%);
@@ -35,19 +35,20 @@
 .panel-title {
   position: relative;
   display: flex;
-  padding: 0 16px 0 36px;
+  height: calc(36px * var(--kb-scale, 1));
+  padding: 0 calc(16px * var(--kb-scale, 1)) 0 calc(36px * var(--kb-scale, 1));
   font-family: YouSheBiaoTiHei, sans-serif;
-  font-size: 24px;
+  font-size: calc(24px * var(--kb-scale, 1));
   letter-spacing: 1px;
   color: #03409b;
   align-items: center;
 
   &::after {
     position: absolute;
-    right: 10px;
+    right: calc(10px * var(--kb-scale, 1));
     bottom: 0;
-    left: 10px;
-    height: 2px;
+    left: calc(10px * var(--kb-scale, 1));
+    height: calc(2px * var(--kb-scale, 1));
     background: linear-gradient(
       to right,
       rgb(3 64 155 / 15%) 0%,
@@ -60,46 +61,135 @@
 
   &::before {
     position: absolute;
-    right: 10px;
+    right: calc(10px * var(--kb-scale, 1));
     bottom: 0;
     z-index: 1;
-    width: 80px;
-    height: 4px;
+    width: calc(80px * var(--kb-scale, 1));
+    height: calc(4px * var(--kb-scale, 1));
     background: #03409b;
-    border-radius: 2px 2px 0 0;
+    border-radius: calc(2px * var(--kb-scale, 1)) calc(2px * var(--kb-scale, 1)) 0 0;
     content: '';
   }
 
   .icon-decorator {
     position: absolute;
-    left: 14px;
+    left: calc(14px * var(--kb-scale, 1));
     display: flex;
     align-items: center;
-    gap: 3px;
+    gap: calc(3px * var(--kb-scale, 1));
 
     span {
-      width: 4px;
-      height: 18px;
+      width: calc(4px * var(--kb-scale, 1));
+      height: calc(18px * var(--kb-scale, 1));
       background: #03409b;
-      border-radius: 2px;
+      border-radius: calc(2px * var(--kb-scale, 1));
 
       &:last-child {
-        height: 12px;
+        height: calc(12px * var(--kb-scale, 1));
         opacity: 0.5;
       }
     }
   }
 }
 
+.panel-title--lg {
+  height: calc(48px * var(--kb-scale, 1));
+}
+
+.panel-title--md {
+  height: calc(40px * var(--kb-scale, 1));
+}
+
 .kb-panel-title-text {
   font-family: YouSheBiaoTiHei, sans-serif;
-  font-size: 24px;
+  font-size: calc(24px * var(--kb-scale, 1));
   font-weight: normal;
   line-height: 1;
   letter-spacing: 1px;
   color: #03409b;
 }
 
+.summary-panel {
+  height: calc(112px * var(--kb-scale, 1));
+}
+
+.summary-panel__title {
+  height: calc(32px * var(--kb-scale, 1));
+}
+
+.summary-panel__grid {
+  padding: calc(6px * var(--kb-scale, 1)) calc(10px * var(--kb-scale, 1));
+  gap: calc(10px * var(--kb-scale, 1));
+}
+
+.summary-card {
+  gap: calc(8px * var(--kb-scale, 1));
+  padding: calc(8px * var(--kb-scale, 1));
+}
+
+.device-list-panel {
+  height: calc(280px * var(--kb-scale, 1));
+  margin-top: calc(12px * var(--kb-scale, 1));
+}
+
+.device-list-panel__body {
+  padding: calc(8px * var(--kb-scale, 1)) calc(16px * var(--kb-scale, 1));
+}
+
+.device-list-panel__picker {
+  display: flex;
+  width: calc(260px * var(--kb-scale, 1));
+  align-items: center;
+}
+
+.device-list-panel__picker-input {
+  width: calc(260px * var(--kb-scale, 1)) !important;
+
+  :deep(.el-input__wrapper) {
+    min-height: calc(28px * var(--kb-scale, 1));
+    padding: 0 calc(10px * var(--kb-scale, 1));
+  }
+
+  :deep(.el-range-input),
+  :deep(.el-range-separator) {
+    font-size: calc(12px * var(--kb-scale, 1));
+  }
+
+  :deep(.el-range__icon),
+  :deep(.el-range__close-icon) {
+    font-size: calc(14px * var(--kb-scale, 1));
+  }
+}
+
+.kb-inline-picker {
+  display: flex;
+  width: var(--kb-picker-width);
+  align-items: center;
+}
+
+.kb-inline-picker--260 {
+  --kb-picker-width: calc(260px * var(--kb-scale, 1));
+}
+
+.kb-inline-picker__input {
+  width: var(--kb-picker-width) !important;
+
+  :deep(.el-input__wrapper) {
+    min-height: calc(28px * var(--kb-scale, 1));
+    padding: 0 calc(10px * var(--kb-scale, 1));
+  }
+
+  :deep(.el-range-input),
+  :deep(.el-range-separator) {
+    font-size: calc(12px * var(--kb-scale, 1));
+  }
+
+  :deep(.el-range__icon),
+  :deep(.el-range__close-icon) {
+    font-size: calc(14px * var(--kb-scale, 1));
+  }
+}
+
 .summary-card {
   position: relative;
   cursor: default;
@@ -132,11 +222,11 @@
 
 .summary-card::after {
   position: absolute;
-  right: -16px;
-  bottom: -28px;
+  right: calc(-16px * var(--kb-scale, 1));
+  bottom: calc(-28px * var(--kb-scale, 1));
   z-index: 0;
-  width: 84px;
-  height: 84px;
+  width: calc(84px * var(--kb-scale, 1));
+  height: calc(84px * var(--kb-scale, 1));
   pointer-events: none;
   background: radial-gradient(circle, var(--card-glow) 0%, rgb(255 255 255 / 0%) 70%);
   content: '';
@@ -188,11 +278,11 @@
   position: relative;
   z-index: 2;
   display: flex;
-  width: 48px;
-  height: 48px;
+  width: calc(48px * var(--kb-scale, 1));
+  height: calc(48px * var(--kb-scale, 1));
   background: linear-gradient(180deg, rgb(255 255 255 / 96%) 0%, rgb(242 247 255 / 92%) 100%);
   border: 1px solid rgb(255 255 255 / 88%);
-  border-radius: 12px;
+  border-radius: calc(12px * var(--kb-scale, 1));
   box-shadow:
     inset 0 1px 0 rgb(255 255 255 / 95%),
     0 6px 12px rgb(152 181 230 / 15%);
@@ -206,10 +296,10 @@
 
 .summary-card__icon::after {
   position: absolute;
-  right: -3px;
-  bottom: -3px;
-  width: 15px;
-  height: 15px;
+  right: calc(-3px * var(--kb-scale, 1));
+  bottom: calc(-3px * var(--kb-scale, 1));
+  width: calc(15px * var(--kb-scale, 1));
+  height: calc(15px * var(--kb-scale, 1));
   background: radial-gradient(
     circle at 30% 30%,
     rgb(255 255 255 / 96%) 0%,
@@ -231,7 +321,7 @@
 .summary-card__icon-glyph {
   position: relative;
   z-index: 1;
-  font-size: 25px;
+  font-size: calc(25px * var(--kb-scale, 1));
   color: var(--card-accent);
   transition:
     transform 0.28s ease,
@@ -247,7 +337,7 @@
   position: relative;
   z-index: 2;
   display: flex;
-  min-height: 48px;
+  min-height: calc(48px * var(--kb-scale, 1));
   min-width: 0;
   flex-direction: column;
   justify-content: center;
@@ -255,7 +345,7 @@
 
 .summary-card__label {
   overflow: hidden;
-  font-size: 14px;
+  font-size: calc(14px * var(--kb-scale, 1));
   font-weight: 600;
   color: #24364f;
   text-overflow: ellipsis;
@@ -266,9 +356,9 @@
 }
 
 .summary-card__value {
-  margin-top: 4px;
+  margin-top: calc(4px * var(--kb-scale, 1));
   font-family: YouSheBiaoTiHei, sans-serif;
-  font-size: 19px;
+  font-size: calc(19px * var(--kb-scale, 1));
   line-height: 1;
   letter-spacing: 1px;
   color: #1f5bb8;
@@ -292,8 +382,8 @@
   top: 0;
   right: 0;
   z-index: 1;
-  width: 36px;
-  height: 36px;
+  width: calc(36px * var(--kb-scale, 1));
+  height: calc(36px * var(--kb-scale, 1));
   pointer-events: none;
   background: linear-gradient(135deg, rgb(255 255 255 / 30%) 0%, rgb(255 255 255 / 0%) 70%);
   opacity: 0.85;
@@ -359,7 +449,7 @@
 .summary-card__placeholder {
   display: inline-block;
   font-family: YouSheBiaoTiHei, sans-serif;
-  font-size: 19px;
+  font-size: calc(19px * var(--kb-scale, 1));
   line-height: 1;
   letter-spacing: 1px;
   color: rgb(31 91 184 / 45%);
@@ -383,7 +473,7 @@
 
   :deep(.el-table__header-wrapper th.el-table__cell) {
     // font-family: YouSheBiaoTiHei, sans-serif;
-    font-size: 20px;
+    font-size: calc(20px * var(--kb-scale, 1));
     font-weight: 500;
     letter-spacing: 0.5px;
     color: #03409b;
@@ -392,9 +482,9 @@
   }
 
   :deep(.el-table__body td.el-table__cell) {
-    padding: 8px 0;
+    padding: calc(8px * var(--kb-scale, 1)) 0;
     // font-family: YouSheBiaoTiHei, sans-serif;
-    font-size: 16px;
+    font-size: calc(16px * var(--kb-scale, 1));
     color: #24364f;
     background: rgb(255 255 255 / 16%);
     border-bottom: 1px solid rgb(31 91 184 / 8%);
@@ -409,11 +499,11 @@
   }
 
   :deep(.el-scrollbar__bar.is-vertical) {
-    width: 8px;
+    width: calc(8px * var(--kb-scale, 1));
   }
 
   :deep(.el-scrollbar__bar.is-horizontal) {
-    height: 8px;
+    height: calc(8px * var(--kb-scale, 1));
   }
 
   :deep(.el-scrollbar__thumb) {
@@ -454,7 +544,7 @@
     rgb(255 255 255 / 0%) 68%,
     rgb(255 255 255 / 0%) 100%
   );
-  border-radius: 20px;
+  border-radius: calc(20px * var(--kb-scale, 1));
   content: '';
   opacity: 0;
   transform: translateX(-140%) skewX(-22deg);
@@ -465,11 +555,11 @@
 
 .kb-stage-card::after {
   position: absolute;
-  right: -24px;
-  bottom: -30px;
+  right: calc(-24px * var(--kb-scale, 1));
+  bottom: calc(-30px * var(--kb-scale, 1));
   z-index: 0;
-  width: 120px;
-  height: 120px;
+  width: calc(120px * var(--kb-scale, 1));
+  height: calc(120px * var(--kb-scale, 1));
   pointer-events: none;
   background: radial-gradient(circle, var(--panel-glow) 0%, rgb(255 255 255 / 0%) 72%);
   content: '';

+ 41 - 19
src/views/Home/Index.vue

@@ -7,34 +7,27 @@ import hsafe from './kb/hsafe.vue'
 import hdeviceStatus from './kb/hdeviceStatus.vue'
 import horderTrend from './kb/horderTrend.vue'
 
-const company = ref('首页')
+const company = ref('首页123')
 
 const wrapperRef = ref<HTMLDivElement>()
 const scale = ref(1)
-const supportsZoom = ref(false)
 
 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(() => {
-  const style = {
-    width: `${DESIGN_WIDTH}px`,
-    height: `${DESIGN_HEIGHT}px`,
-    transformOrigin: '0 0'
-  } as Record<string, string | number>
-
-  if (supportsZoom.value) {
-    style.zoom = scale.value
-  } else {
-    style.transform = `scale(${scale.value})`
+  return {
+    '--kb-scale': scale.value,
+    width: `${DESIGN_WIDTH * scale.value}px`,
+    height: `${DESIGN_HEIGHT * scale.value}px`
   }
-
-  return style
 })
 
 function updateScale() {
@@ -48,11 +41,13 @@ function updateScale() {
     if (!clientWidth || !clientHeight) return
 
     scale.value = Math.min(clientWidth / DESIGN_WIDTH, clientHeight / DESIGN_HEIGHT)
+    nextTick(() => {
+      window.dispatchEvent(new Event('homekb:resize'))
+    })
   })
 }
 
 onMounted(() => {
-  supportsZoom.value = typeof CSS !== 'undefined' && CSS.supports?.('zoom', '1') === true
   nextTick(updateScale)
   resizeObserver = new ResizeObserver(updateScale)
   if (wrapperRef.value) {
@@ -70,17 +65,17 @@ onUnmounted(() => {
 <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" :style="targetAreaStyle">
+      <div class="bg kb-screen" :style="targetAreaStyle">
         <header class="header">{{ company }}</header>
-        <div class="mt-3 px-5">
+        <div class="kb-content">
           <hsummary class="kb-stage-card kb-stage-card--1" />
-          <div class="w-full h-96 grid grid-rows-2 grid-cols-3 gap-3 mt-3">
+          <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="w-full h-107 grid grid-cols-2 gap-3 mt-3">
+          <div class="home-bottom-grid">
             <hdeviceStatus class="kb-stage-card kb-stage-card--6" />
             <horderTrend class="kb-stage-card kb-stage-card--7" />
           </div>
@@ -91,4 +86,31 @@ onUnmounted(() => {
 </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>

+ 5 - 3
src/views/Home/kb/dayfinish.vue

@@ -203,17 +203,19 @@ onMounted(() => {
   initChart()
   loadChart()
   window.addEventListener('resize', resizeChart)
+  window.addEventListener('homekb:resize', resizeChart)
 })
 
 onUnmounted(() => {
   window.removeEventListener('resize', resizeChart)
+  window.removeEventListener('homekb:resize', resizeChart)
   destroyChart()
 })
 </script>
 
 <template>
   <div class="panel flex flex-col">
-    <div class="panel-title h-12 flex items-center justify-between">
+    <div class="panel-title panel-title--lg flex items-center justify-between">
       <div class="kb-panel-title-text flex items-center">
         <div class="icon-decorator">
           <span></span>
@@ -221,7 +223,7 @@ onUnmounted(() => {
         </div>
         日报完成率
       </div>
-      <div class="w-260px! -translate-y-[2px]">
+      <div class="kb-inline-picker kb-inline-picker--260">
         <el-date-picker
           v-model="createTime"
           value-format="YYYY-MM-DD HH:mm:ss"
@@ -231,7 +233,7 @@ onUnmounted(() => {
           :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
           :shortcuts="rangeShortcuts"
           :clearable="false"
-          class="w-260px!"
+          class="kb-inline-picker__input"
           @change="handleDateChange" />
       </div>
     </div>

+ 3 - 1
src/views/Home/kb/hdeviceStatus.vue

@@ -108,17 +108,19 @@ onMounted(() => {
   initChart()
   loadChart()
   window.addEventListener('resize', resizeChart)
+  window.addEventListener('homekb:resize', resizeChart)
 })
 
 onUnmounted(() => {
   window.removeEventListener('resize', resizeChart)
+  window.removeEventListener('homekb:resize', resizeChart)
   destroyChart()
 })
 </script>
 
 <template>
   <div class="panel flex flex-col">
-    <div class="panel-title h-9">
+    <div class="panel-title">
       <div class="icon-decorator">
         <span></span>
         <span></span>

+ 3 - 1
src/views/Home/kb/hdeviceType.vue

@@ -152,17 +152,19 @@ onMounted(() => {
   initChart()
   loadChart()
   window.addEventListener('resize', resizeChart)
+  window.addEventListener('homekb:resize', resizeChart)
 })
 
 onUnmounted(() => {
   window.removeEventListener('resize', resizeChart)
+  window.removeEventListener('homekb:resize', resizeChart)
   destroyChart()
 })
 </script>
 
 <template>
   <div class="panel flex flex-col">
-    <div class="panel-title h-12">
+    <div class="panel-title panel-title--lg">
       <div class="icon-decorator">
         <span></span>
         <span></span>

+ 14 - 13
src/views/Home/kb/hmttr.vue

@@ -31,7 +31,7 @@ onMounted(() => {
 
 <template>
   <div class="panel flex flex-col">
-    <div class="panel-title h-10">
+    <div class="panel-title panel-title--md">
       <div class="icon-decorator">
         <span></span>
         <span></span>
@@ -78,7 +78,8 @@ onMounted(() => {
 .metric-panel {
   position: relative;
   display: flex;
-  padding: 12px 12px 9px;
+  padding: calc(12px * var(--kb-scale, 1)) calc(12px * var(--kb-scale, 1))
+    calc(9px * var(--kb-scale, 1));
   overflow: hidden;
   flex-direction: column;
 }
@@ -92,9 +93,9 @@ onMounted(() => {
 }
 
 .metric-panel__orbit--outer {
-  top: 20px;
-  width: 180px;
-  height: 180px;
+  top: calc(20px * var(--kb-scale, 1));
+  width: calc(180px * var(--kb-scale, 1));
+  height: calc(180px * var(--kb-scale, 1));
 }
 
 .metric-panel__top,
@@ -105,9 +106,9 @@ onMounted(() => {
 
 .metric-panel__tag {
   display: inline-flex;
-  padding: 4px 10px;
+  padding: calc(4px * var(--kb-scale, 1)) calc(10px * var(--kb-scale, 1));
   font-family: YouSheBiaoTiHei, sans-serif;
-  font-size: 14px;
+  font-size: calc(14px * var(--kb-scale, 1));
   line-height: 1;
   letter-spacing: 1px;
   color: var(--metric-accent);
@@ -127,7 +128,7 @@ onMounted(() => {
 }
 
 .metric-panel__headline {
-  font-size: 16px;
+  font-size: calc(16px * var(--kb-scale, 1));
   font-weight: 600;
   letter-spacing: 1px;
   color: #4f678a;
@@ -135,14 +136,14 @@ onMounted(() => {
 
 .metric-panel__value-row {
   display: flex;
-  margin-top: 10px;
+  margin-top: calc(10px * var(--kb-scale, 1));
   align-items: flex-end;
   justify-content: center;
 }
 
 .metric-panel__value {
   font-family: YouSheBiaoTiHei, sans-serif;
-  font-size: 54px;
+  font-size: calc(54px * var(--kb-scale, 1));
   line-height: 0.92;
   letter-spacing: 1px;
   color: var(--metric-accent);
@@ -150,10 +151,10 @@ onMounted(() => {
 }
 
 .metric-panel__unit {
-  padding-bottom: 7px;
-  margin-left: 4px;
+  padding-bottom: calc(7px * var(--kb-scale, 1));
+  margin-left: calc(4px * var(--kb-scale, 1));
   font-family: YouSheBiaoTiHei, sans-serif;
-  font-size: 22px;
+  font-size: calc(22px * var(--kb-scale, 1));
   line-height: 1;
   color: #5b789e;
 }

+ 3 - 1
src/views/Home/kb/horderTrend.vue

@@ -185,17 +185,19 @@ onMounted(() => {
   initChart()
   loadChart()
   window.addEventListener('resize', resizeChart)
+  window.addEventListener('homekb:resize', resizeChart)
 })
 
 onUnmounted(() => {
   window.removeEventListener('resize', resizeChart)
+  window.removeEventListener('homekb:resize', resizeChart)
   destroyChart()
 })
 </script>
 
 <template>
   <div class="panel flex flex-col">
-    <div class="panel-title h-9">
+    <div class="panel-title">
       <div class="icon-decorator">
         <span></span>
         <span></span>

+ 15 - 13
src/views/Home/kb/hsafe.vue

@@ -31,7 +31,7 @@ onMounted(() => {
 
 <template>
   <div class="panel flex flex-col">
-    <div class="panel-title h-12">
+    <div class="panel-title panel-title--lg">
       <div class="icon-decorator">
         <span></span>
         <span></span>
@@ -79,15 +79,17 @@ onMounted(() => {
 .metric-panel {
   position: relative;
   display: grid;
-  padding: 16px 14px 20px;
+  padding: calc(16px * var(--kb-scale, 1)) calc(14px * var(--kb-scale, 1))
+    calc(20px * var(--kb-scale, 1));
   overflow: hidden;
   grid-template-rows: auto minmax(0, 1fr);
-  gap: 16px;
+  gap: calc(16px * var(--kb-scale, 1));
 }
 
 .metric-panel::before {
   position: absolute;
-  inset: 54px 18px 16px;
+  inset: calc(54px * var(--kb-scale, 1)) calc(18px * var(--kb-scale, 1))
+    calc(16px * var(--kb-scale, 1));
   pointer-events: none;
   background: radial-gradient(
     circle at center,
@@ -107,13 +109,13 @@ onMounted(() => {
 }
 
 .metric-panel__orbit--outer {
-  width: 240px;
-  height: 240px;
+  width: calc(240px * var(--kb-scale, 1));
+  height: calc(240px * var(--kb-scale, 1));
 }
 
 .metric-panel__orbit--inner {
-  width: 188px;
-  height: 188px;
+  width: calc(188px * var(--kb-scale, 1));
+  height: calc(188px * var(--kb-scale, 1));
   border-color: rgb(255 255 255 / 42%);
   opacity: 0.55;
 }
@@ -134,9 +136,9 @@ onMounted(() => {
 
 .metric-panel__tag {
   display: inline-flex;
-  padding: 5px 12px;
+  padding: calc(5px * var(--kb-scale, 1)) calc(12px * var(--kb-scale, 1));
   font-family: YouSheBiaoTiHei, sans-serif;
-  font-size: 16px;
+  font-size: calc(16px * var(--kb-scale, 1));
   line-height: 1;
   letter-spacing: 2px;
   color: var(--metric-accent);
@@ -156,7 +158,7 @@ onMounted(() => {
 }
 
 .metric-panel__headline {
-  font-size: 22px;
+  font-size: calc(22px * var(--kb-scale, 1));
   font-weight: 600;
   letter-spacing: 2px;
   color: #4f678a;
@@ -165,14 +167,14 @@ onMounted(() => {
 
 .metric-panel__value-row {
   display: flex;
-  margin-top: 18px;
+  margin-top: calc(18px * var(--kb-scale, 1));
   align-items: flex-end;
   justify-content: center;
 }
 
 .metric-panel__value {
   font-family: YouSheBiaoTiHei, sans-serif;
-  font-size: 90px;
+  font-size: calc(90px * var(--kb-scale, 1));
   line-height: 0.88;
   letter-spacing: 2px;
   color: var(--metric-accent);

+ 4 - 4
src/views/Home/kb/hsummary.vue

@@ -224,8 +224,8 @@ onMounted(() => {
 </script>
 
 <template>
-  <div class="panel w-full h-28 flex flex-col">
-    <div class="panel-title h-8">
+  <div class="panel summary-panel w-full flex flex-col">
+    <div class="panel-title summary-panel__title">
       <div class="icon-decorator">
         <span></span>
         <span></span>
@@ -233,11 +233,11 @@ onMounted(() => {
       工单情况
     </div>
 
-    <div class="grid grid-cols-8 gap-2.5 flex-1 px-2.5 py-1.5">
+    <div class="summary-panel__grid grid grid-cols-8 flex-1">
       <article
         v-for="card in summaryCards"
         :key="card.key"
-        class="summary-card relative flex h-full overflow-hidden rounded-md items-center gap-2 p-2"
+        class="summary-card relative flex h-full overflow-hidden rounded-md items-center"
         :style="{
           '--card-accent': card.accent,
           '--card-glow': card.glow

+ 205 - 156
src/views/pms/operation-meeting/components/operation-meeting-content.vue

@@ -141,8 +141,15 @@ const meetingTableBlueColumns = new Set<keyof DetailItem>([
 
 const isSummaryMode = computed(() => props.mode === 'summary')
 const canEditDetails = computed(() => !isSummaryMode.value && props.type !== 'view')
-const detailPanelVisible = ref(false)
+const detailPanelVisible = ref(props.mode === 'summary')
 const workloadDetailVisible = ref(false)
+const meetingHeaderVisible = computed(() => detailPanelVisible.value)
+
+watch(isSummaryMode, (summaryMode) => {
+  if (summaryMode) {
+    detailPanelVisible.value = true
+  }
+})
 
 const activeMeetings = computed<OperationMeetingFormItem[]>(() => {
   if (props.meetings.length) return props.meetings
@@ -715,168 +722,172 @@ const getMeetingTableCellClassName = ({ column }: MeetingTableCellStyleProps) =>
 
 <template>
   <section class="p-2 bg-white border-solid border-1 border-gray-200/90 rounded-xl">
-    <div class="meeting-section__top">
-      <h2 class="meeting-section__title">生产运营双周例会汇报</h2>
-
-      <div
-        :class="[
-          'meeting-section__grid',
-          { 'meeting-section__grid--company-only': !showMeetingMetaFields }
-        ]">
-        <el-form-item
-          v-if="showMeetingMetaFields"
-          label="会议期次"
-          label-position="left"
-          prop="meetingSeries"
-          class="mb-0! min-w-0">
-          <el-input-number
-            v-if="isSummaryMode"
-            :model-value="summaryMeetingMeta.meetingSeries"
-            class="w-full!"
-            placeholder="暂无会议期次"
-            disabled
-            :controls="false" />
-          <el-input-number
-            v-else
-            v-model="meetingSeriesModel"
-            class="w-full!"
-            placeholder="请输入会议期次"
-            :disabled="type === 'view'"
-            :controls="false"
-            :min="1"
-            :step="1"
-            :precision="0" />
-        </el-form-item>
-
-        <el-form-item label="专业公司" label-position="left" class="mb-0! min-w-0">
-          <el-select
-            v-if="isSummaryMode"
-            v-model="companyFilterModel"
-            class="w-full!"
-            placeholder="全部公司"
-            clearable
-            filterable>
-            <el-option
-              v-for="item in companyOptions"
-              :key="item.value"
-              :label="item.label"
-              :value="item.value" />
-          </el-select>
-          <el-input
-            v-else
-            :model-value="companyDisplayName"
-            class="w-full!"
-            placeholder="新建保存后系统自动填充"
-            disabled />
-          <div v-if="!isSummaryMode && type === 'create'" class="absolute text-xs text-gray top-8">
-            新建保存后由系统自动填充。
-          </div>
-        </el-form-item>
-
-        <el-form-item
-          v-if="showMeetingMetaFields"
-          label="会议日期"
-          label-position="left"
-          prop="meetingDate"
-          class="mb-0! min-w-0">
-          <el-date-picker
-            v-if="isSummaryMode"
-            :model-value="summaryMeetingMeta.meetingDate"
-            type="date"
-            placeholder="暂无会议日期"
-            disabled
-            class="w-full!" />
-          <el-date-picker
-            v-else
-            v-model="meetingDateModel"
-            type="date"
-            placeholder="请选择会议日期"
-            :disabled="type === 'view'"
-            class="w-full!" />
-        </el-form-item>
-      </div>
-    </div>
+    <div v-show="meetingHeaderVisible">
+      <div class="meeting-section__top">
+        <h2 class="meeting-section__title">生产运营双周例会汇报</h2>
 
-    <section class="meeting-summary-strip">
-      <div class="meeting-summary-strip__title">
-        <span>{{ isSummaryMode ? currentSummaryScopeName : '公司整体' }}</span>
-        <small>经营数据汇总</small>
-      </div>
-      <div class="meeting-summary-strip__grid">
         <div
-          v-for="item in detailSummaryCards"
-          :key="item.label"
           :class="[
-            'meeting-summary-strip__item',
-            `meeting-summary-strip__item--${item.tone}`,
-            { 'meeting-summary-strip__item--no-compare': !item.compare }
+            'meeting-section__grid',
+            { 'meeting-section__grid--company-only': !showMeetingMetaFields }
           ]">
-          <div :class="item.icon + ' size-5 icon'"></div>
-          <span>{{ item.label }}</span>
-          <strong>{{ item.value }}<em>万元</em></strong>
-          <small
-            v-if="item.compare"
-            :class="[
-              'meeting-summary-strip__compare',
-              `meeting-summary-strip__compare--${item.compare.trend}`
-            ]">
-            <i :class="[item.compare.icon, 'meeting-summary-strip__compare-icon']"></i>
-            环比 {{ item.compare.value }}
-          </small>
+          <el-form-item
+            v-if="showMeetingMetaFields"
+            label="会议期次"
+            label-position="left"
+            prop="meetingSeries"
+            class="mb-0! min-w-0">
+            <el-input-number
+              v-if="isSummaryMode"
+              :model-value="summaryMeetingMeta.meetingSeries"
+              class="w-full!"
+              placeholder="暂无会议期次"
+              disabled
+              :controls="false" />
+            <el-input-number
+              v-else
+              v-model="meetingSeriesModel"
+              class="w-full!"
+              placeholder="请输入会议期次"
+              :disabled="type === 'view'"
+              :controls="false"
+              :min="1"
+              :step="1"
+              :precision="0" />
+          </el-form-item>
+
+          <el-form-item label="专业公司" label-position="left" class="mb-0! min-w-0">
+            <el-select
+              v-if="isSummaryMode"
+              v-model="companyFilterModel"
+              class="w-full!"
+              placeholder="全部公司"
+              clearable
+              filterable>
+              <el-option
+                v-for="item in companyOptions"
+                :key="item.value"
+                :label="item.label"
+                :value="item.value" />
+            </el-select>
+            <el-input
+              v-else
+              :model-value="companyDisplayName"
+              class="w-full!"
+              placeholder="新建保存后系统自动填充"
+              disabled />
+            <div
+              v-if="!isSummaryMode && type === 'create'"
+              class="absolute text-xs text-gray top-8">
+              新建保存后由系统自动填充。
+            </div>
+          </el-form-item>
+
+          <el-form-item
+            v-if="showMeetingMetaFields"
+            label="会议日期"
+            label-position="left"
+            prop="meetingDate"
+            class="mb-0! min-w-0">
+            <el-date-picker
+              v-if="isSummaryMode"
+              :model-value="summaryMeetingMeta.meetingDate"
+              type="date"
+              placeholder="暂无会议日期"
+              disabled
+              class="w-full!" />
+            <el-date-picker
+              v-else
+              v-model="meetingDateModel"
+              type="date"
+              placeholder="请选择会议日期"
+              :disabled="type === 'view'"
+              class="w-full!" />
+          </el-form-item>
         </div>
       </div>
-    </section>
 
-    <section v-if="showSummaryWorkload" class="meeting-workload-strip">
-      <div class="meeting-workload-strip__title">
-        <span>{{ workloadScopeName }}</span>
-        <small>工作量完成情况</small>
-        <el-button
-          class="meeting-workload-strip__detail"
-          link
-          size="small"
-          type="primary"
-          @click="workloadDetailVisible = true">
-          查看详情
-        </el-button>
-      </div>
-      <div
-        :class="[
-          'meeting-workload-strip__content',
-          { 'meeting-workload-strip__content--split': splitSummaryWorkload }
-        ]">
-        <div class="meeting-workload-strip__grid">
+      <section class="meeting-summary-strip">
+        <div class="meeting-summary-strip__title">
+          <span>{{ isSummaryMode ? currentSummaryScopeName : '公司整体' }}</span>
+          <small>经营数据汇总</small>
+        </div>
+        <div class="meeting-summary-strip__grid">
           <div
-            v-for="(item, index) in displayedSummaryWorkloadProperties"
-            :key="item.identifier"
+            v-for="item in detailSummaryCards"
+            :key="item.label"
             :class="[
-              'meeting-workload-strip__item',
-              `meeting-workload-strip__item--${getSummaryWorkloadTone(item, index)}`
+              'meeting-summary-strip__item',
+              `meeting-summary-strip__item--${item.tone}`,
+              { 'meeting-summary-strip__item--no-compare': !item.compare }
             ]">
-            <div :class="[getSummaryWorkloadIcon(item), 'size-5 icon']"></div>
-            <span>{{ item.name }}</span>
-            <strong>{{ formatWorkloadPropertyValue(item) }}</strong>
+            <div :class="item.icon + ' size-5 icon'"></div>
+            <span>{{ item.label }}</span>
+            <strong>{{ item.value }}<em>万元</em></strong>
+            <small
+              v-if="item.compare"
+              :class="[
+                'meeting-summary-strip__compare',
+                `meeting-summary-strip__compare--${item.compare.trend}`
+              ]">
+              <i :class="[item.compare.icon, 'meeting-summary-strip__compare-icon']"></i>
+              环比 {{ item.compare.value }}
+            </small>
           </div>
         </div>
-        <div v-if="splitSummaryWorkload" class="meeting-workload-strip__side">
-          <div
-            v-for="(item, index) in sideSummaryWorkloadProperties"
-            :key="item.identifier"
-            :class="[
-              'meeting-workload-strip__item',
-              'meeting-workload-strip__item--side',
-              `meeting-workload-strip__item--${getSummaryWorkloadTone(
-                item,
-                index + primarySummaryWorkloadProperties.length
-              )}`
-            ]">
-            <div :class="[getSummaryWorkloadIcon(item), 'size-5 icon']"></div>
-            <span>{{ item.name }}</span>
-            <strong>{{ formatWorkloadPropertyValue(item) }}</strong>
+      </section>
+
+      <section v-if="showSummaryWorkload" class="meeting-workload-strip">
+        <div class="meeting-workload-strip__title">
+          <span>{{ workloadScopeName }}</span>
+          <small>工作量完成情况</small>
+          <el-button
+            class="meeting-workload-strip__detail"
+            link
+            size="small"
+            type="primary"
+            @click="workloadDetailVisible = true">
+            查看详情
+          </el-button>
+        </div>
+        <div
+          :class="[
+            'meeting-workload-strip__content',
+            { 'meeting-workload-strip__content--split': splitSummaryWorkload }
+          ]">
+          <div class="meeting-workload-strip__grid">
+            <div
+              v-for="(item, index) in displayedSummaryWorkloadProperties"
+              :key="item.identifier"
+              :class="[
+                'meeting-workload-strip__item',
+                `meeting-workload-strip__item--${getSummaryWorkloadTone(item, index)}`
+              ]">
+              <div :class="[getSummaryWorkloadIcon(item), 'size-5 icon']"></div>
+              <span>{{ item.name }}</span>
+              <strong>{{ formatWorkloadPropertyValue(item) }}</strong>
+            </div>
+          </div>
+          <div v-if="splitSummaryWorkload" class="meeting-workload-strip__side">
+            <div
+              v-for="(item, index) in sideSummaryWorkloadProperties"
+              :key="item.identifier"
+              :class="[
+                'meeting-workload-strip__item',
+                'meeting-workload-strip__item--side',
+                `meeting-workload-strip__item--${getSummaryWorkloadTone(
+                  item,
+                  index + primarySummaryWorkloadProperties.length
+                )}`
+              ]">
+              <div :class="[getSummaryWorkloadIcon(item), 'size-5 icon']"></div>
+              <span>{{ item.name }}</span>
+              <strong>{{ formatWorkloadPropertyValue(item) }}</strong>
+            </div>
           </div>
         </div>
-      </div>
-    </section>
+      </section>
+    </div>
 
     <el-dialog
       v-model="workloadDetailVisible"
@@ -903,10 +914,13 @@ const getMeetingTableCellClassName = ({ column }: MeetingTableCellStyleProps) =>
       </div>
     </el-dialog>
 
-    <section class="meeting-detail-panel">
+    <section
+      :class="[
+        'meeting-detail-panel',
+        { 'meeting-detail-panel--collapsed': !meetingHeaderVisible }
+      ]">
       <el-button
-        v-if="!isSummaryMode"
-        class="mb-2"
+        :class="['meeting-detail-toggle', { 'mb-2': meetingHeaderVisible }]"
         size="default"
         link
         @click="() => (detailPanelVisible = !detailPanelVisible)">
@@ -919,9 +933,7 @@ const getMeetingTableCellClassName = ({ column }: MeetingTableCellStyleProps) =>
           <!-- {{ visible ? '收起' : '展开' }} -->
         </div>
       </el-button>
-      <div
-        v-show="isSummaryMode || detailPanelVisible"
-        class="flex items-center justify-between gap-4 mb-4">
+      <div v-if="meetingHeaderVisible" class="flex items-center justify-between gap-4 mb-4">
         <h3 class="text-lg font-bold m-0">会议明细</h3>
         <el-tag v-if="isSummaryMode" size="small" effect="plain">
           共 {{ displayDetails.length }} 项
@@ -936,7 +948,6 @@ const getMeetingTableCellClassName = ({ column }: MeetingTableCellStyleProps) =>
           class="meeting-table"
           :data="displayDetails"
           :loading="loading"
-          :max-height="660"
           align="left"
           :show-overflow-tooltip="false"
           :cell-style="getMeetingTableCellStyle"
@@ -1490,6 +1501,18 @@ const getMeetingTableCellClassName = ({ column }: MeetingTableCellStyleProps) =>
   border-top: 1px solid var(--el-border-color-lighter);
 }
 
+.meeting-detail-panel--collapsed {
+  padding-top: 0;
+  margin-top: 0;
+  border-top: 0;
+}
+
+.meeting-detail-toggle {
+  display: flex;
+  align-items: center;
+  min-height: 28px;
+}
+
 .meeting-detail-toggle__icon {
   transition: transform var(--el-transition-duration) ease;
 }
@@ -1505,9 +1528,35 @@ const getMeetingTableCellClassName = ({ column }: MeetingTableCellStyleProps) =>
   --zm-table-row-font-weight: 800;
   --zm-table-header-text-color: #333;
   --zm-table-border-color: #cbd5e1;
+  --zm-table-radius: 0;
   --zm-table-header-border-color: #c2ccda;
   --zm-table-row-border-color: #d4dce8;
 
+  overflow: visible;
+
+  :deep(.el-table__inner-wrapper) {
+    overflow: visible;
+  }
+
+  :deep(.el-table__body-wrapper),
+  :deep(.el-scrollbar),
+  :deep(.el-scrollbar__wrap) {
+    overflow: visible;
+  }
+
+  :deep(.el-scrollbar__bar.is-horizontal) {
+    display: block;
+  }
+
+  :deep(.el-table__header-wrapper),
+  :deep(.el-table__fixed-header-wrapper) {
+    position: sticky !important;
+    top: -20px;
+    z-index: 20;
+    width: 100%;
+    background: #fff;
+  }
+
   :deep(.header-wrapper) {
     height: 20px;
     justify-content: center !important;

+ 30 - 17
src/views/pms/stat/rhkb.vue

@@ -18,30 +18,23 @@ const company = ref('瑞恒')
 
 const wrapperRef = ref<HTMLDivElement>()
 const scale = ref(1)
-const supportsZoom = ref(false)
 
 let resizeObserver: ResizeObserver | null = null
 let resizeRaf = 0
 
+provide('rhKbScale', scale)
+
 const targetWrapperStyle = computed(() => ({
   width: `${DESIGN_WIDTH * scale.value}px`,
   height: `${DESIGN_HEIGHT * scale.value}px`
 }))
 
 const targetAreaStyle = computed(() => {
-  const style = {
-    width: `${DESIGN_WIDTH}px`,
-    height: `${DESIGN_HEIGHT}px`,
-    transformOrigin: '0 0'
-  } as Record<string, string | number>
-
-  if (supportsZoom.value) {
-    style.zoom = scale.value
-  } else {
-    style.transform = `scale(${scale.value})`
+  return {
+    '--kb-scale': scale.value,
+    width: `${DESIGN_WIDTH * scale.value}px`,
+    height: `${DESIGN_HEIGHT * scale.value}px`
   }
-
-  return style
 })
 
 function updateScale() {
@@ -55,11 +48,13 @@ function updateScale() {
     if (!clientWidth || !clientHeight) return
 
     scale.value = Math.min(clientWidth / DESIGN_WIDTH, clientHeight / DESIGN_HEIGHT)
+    nextTick(() => {
+      window.dispatchEvent(new Event('rhkb:resize'))
+    })
   })
 }
 
 onMounted(() => {
-  supportsZoom.value = typeof CSS !== 'undefined' && CSS.supports?.('zoom', '1') === true
   nextTick(updateScale)
   resizeObserver = new ResizeObserver(updateScale)
   if (wrapperRef.value) {
@@ -78,11 +73,11 @@ onUnmounted(() => {
 <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" id="rhkb" :style="targetAreaStyle">
+      <div class="bg kb-screen" id="rhkb" :style="targetAreaStyle">
         <header class="header">{{ company }}</header>
-        <div class="mt-3 px-5">
+        <div class="kb-content">
           <rhsummary class="kb-stage-card kb-stage-card--1" />
-          <div class="w-full h-148 grid grid-rows-2 grid-cols-3 gap-3 mt-3">
+          <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" />
@@ -99,4 +94,22 @@ onUnmounted(() => {
 
 <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;
+}
+
+.kb-chart-grid {
+  display: grid;
+  width: 100%;
+  height: calc(592px * var(--kb-scale));
+  margin-top: calc(12px * var(--kb-scale));
+  gap: calc(12px * var(--kb-scale));
+  grid-template-rows: repeat(2, minmax(0, 1fr));
+  grid-template-columns: repeat(3, minmax(0, 1fr));
+}
 </style>

+ 9 - 6
src/views/pms/stat/rhkb/deviceList.vue

@@ -2,6 +2,7 @@
 import { IotStatApi } from '@/api/pms/stat'
 import { rangeShortcuts } from '@/utils/formatTime'
 import dayjs from 'dayjs'
+import type { Ref } from 'vue'
 
 interface RhDeviceListRow {
   projectDeptId: number
@@ -34,8 +35,10 @@ const teamDialogVisible = ref(false)
 const teamLoading = ref(false)
 const currentProjectDeptName = ref('')
 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))
 
 function formatRate(value?: number | null) {
   return `${(Number(value ?? 0) * 100).toFixed(2)}%`
@@ -94,8 +97,8 @@ onMounted(() => {
 </script>
 
 <template>
-  <div class="panel w-full h-[280px] flex flex-col mt-3">
-    <div class="panel-title h-9 flex items-center justify-between">
+  <div class="panel device-list-panel w-full flex flex-col">
+    <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">
           <span></span>
@@ -103,7 +106,7 @@ onMounted(() => {
         </div>
         项目部统计
       </div>
-      <div class="w-260px! -translate-y-[4px]">
+      <div class="device-list-panel__picker">
         <el-date-picker
           v-model="createTime"
           value-format="YYYY-MM-DD HH:mm:ss"
@@ -113,15 +116,15 @@ onMounted(() => {
           :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
           :clearable="false"
           :shortcuts="rangeShortcuts"
-          class="w-260px!"
+          class="device-list-panel__picker-input"
           @change="handleDateChange" />
       </div>
     </div>
     <!-- v-loading="loading" -->
-    <div class="flex-1 min-h-0 px-4 py-2">
+    <div class="device-list-panel__body flex-1 min-h-0">
       <el-table
         :data="tableData"
-        :height="TABLE_HEIGHT"
+        :height="tableHeight"
         class="device-list-table"
         @row-click="handleRowClick">
         <el-table-column prop="projectDeptName" label="项目部" min-width="220" align="center" />

+ 3 - 1
src/views/pms/stat/rhkb/deviceStatus.vue

@@ -108,17 +108,19 @@ 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 h-9">
+    <div class="panel-title">
       <div class="icon-decorator">
         <span></span>
         <span></span>

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

@@ -151,17 +151,19 @@ 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 h-9">
+    <div class="panel-title">
       <div class="icon-decorator">
         <span></span>
         <span></span>

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

@@ -180,17 +180,19 @@ 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 h-9">
+    <div class="panel-title">
       <div class="icon-decorator">
         <span></span>
         <span></span>

+ 3 - 1
src/views/pms/stat/rhkb/operation.vue

@@ -167,17 +167,19 @@ 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 h-9">
+    <div class="panel-title">
       <div class="icon-decorator">
         <span></span>
         <span></span>

+ 3 - 1
src/views/pms/stat/rhkb/orderTrend.vue

@@ -185,17 +185,19 @@ 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 h-9">
+    <div class="panel-title">
       <div class="icon-decorator">
         <span></span>
         <span></span>

+ 4 - 4
src/views/pms/stat/rhkb/rhsummary.vue

@@ -224,8 +224,8 @@ onMounted(() => {
 </script>
 
 <template>
-  <div class="panel w-full h-28 flex flex-col">
-    <div class="panel-title h-8">
+  <div class="panel summary-panel w-full flex flex-col">
+    <div class="panel-title summary-panel__title">
       <div class="icon-decorator">
         <span></span>
         <span></span>
@@ -233,11 +233,11 @@ onMounted(() => {
       工单情况
     </div>
 
-    <div class="grid grid-cols-8 gap-2.5 flex-1 px-2.5 py-1.5">
+    <div class="summary-panel__grid grid grid-cols-8 flex-1">
       <article
         v-for="card in summaryCards"
         :key="card.key"
-        class="summary-card relative flex h-full overflow-hidden rounded-md items-center gap-2 p-2"
+        class="summary-card relative flex h-full overflow-hidden rounded-md items-center"
         :style="{
           '--card-accent': card.accent,
           '--card-glow': card.glow

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

@@ -191,17 +191,19 @@ 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 h-9">
+    <div class="panel-title">
       <div class="icon-decorator">
         <span></span>
         <span></span>

+ 129 - 25
src/views/pms/stat/rykb.vue

@@ -14,33 +14,32 @@ defineOptions({
 })
 
 const company = ref('瑞鹰')
+const activePage = ref<'home' | 'production'>('home')
+
+const pageTabs = [
+  { label: '看板首页', value: 'home' },
+  { label: '生产日报', value: 'production' }
+] as const
 
 const wrapperRef = ref<HTMLDivElement>()
 const scale = ref(1)
-const supportsZoom = ref(false)
 
 let resizeObserver: ResizeObserver | null = null
 let resizeRaf = 0
 
+provide('ryKbScale', scale)
+
 const targetWrapperStyle = computed(() => ({
   width: `${DESIGN_WIDTH * scale.value}px`,
   height: `${DESIGN_HEIGHT * scale.value}px`
 }))
 
 const targetAreaStyle = computed(() => {
-  const style = {
-    width: `${DESIGN_WIDTH}px`,
-    height: `${DESIGN_HEIGHT}px`,
-    transformOrigin: '0 0'
-  } as Record<string, string | number>
-
-  if (supportsZoom.value) {
-    style.zoom = scale.value
-  } else {
-    style.transform = `scale(${scale.value})`
+  return {
+    '--kb-scale': scale.value,
+    width: `${DESIGN_WIDTH * scale.value}px`,
+    height: `${DESIGN_HEIGHT * scale.value}px`
   }
-
-  return style
 })
 
 function updateScale() {
@@ -54,11 +53,13 @@ function updateScale() {
     if (!clientWidth || !clientHeight) return
 
     scale.value = Math.min(clientWidth / DESIGN_WIDTH, clientHeight / DESIGN_HEIGHT)
+    nextTick(() => {
+      window.dispatchEvent(new Event('rykb:resize'))
+    })
   })
 }
 
 onMounted(() => {
-  supportsZoom.value = typeof CSS !== 'undefined' && CSS.supports?.('zoom', '1') === true
   nextTick(updateScale)
   resizeObserver = new ResizeObserver(updateScale)
   if (wrapperRef.value) {
@@ -67,6 +68,12 @@ onMounted(() => {
   window.addEventListener('resize', updateScale)
 })
 
+watch(activePage, () => {
+  nextTick(() => {
+    window.dispatchEvent(new Event('rykb:resize'))
+  })
+})
+
 onUnmounted(() => {
   resizeObserver?.disconnect()
   window.removeEventListener('resize', updateScale)
@@ -77,19 +84,38 @@ onUnmounted(() => {
 <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" :style="targetAreaStyle">
+      <div class="bg kb-screen" :style="targetAreaStyle">
         <header class="header">{{ company }}</header>
-        <div class="mt-3 px-5">
-          <rysummary class="kb-stage-card kb-stage-card--1" />
-          <div class="w-full h-148 grid grid-rows-2 grid-cols-3 gap-3 mt-3">
-            <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" />
+        <div class="kb-content">
+          <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">
+            <rysummary class="kb-stage-card kb-stage-card--1" />
+            <div class="kb-chart-grid">
+              <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" />
+            </div>
+          </div>
+
+          <div v-else class="kb-production-page">
+            <ryProductionBriefs
+              class="kb-stage-card kb-stage-card--8 kb-stage-card--list"
+              page-mode="full" />
           </div>
-          <ryProductionBriefs class="kb-stage-card kb-stage-card--8 kb-stage-card--list" />
         </div>
       </div>
     </div>
@@ -98,4 +124,82 @@ onUnmounted(() => {
 
 <style scoped lang="scss">
 @import url('@/styles/kb.scss');
+
+.kb-screen {
+  overflow: hidden;
+}
+
+.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));
+}
+
+.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 {
+  min-width: calc(82px * var(--kb-scale));
+  height: calc(28px * 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%;
+  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-columns: repeat(3, minmax(0, 1fr));
+}
+
+.kb-production-page {
+  height: 100%;
+  min-height: 0;
+}
 </style>

+ 73 - 10
src/views/pms/stat/rykb/ryProductionBriefs.vue

@@ -1,6 +1,7 @@
 <script lang="ts" setup>
 import { IotStatApi } from '@/api/pms/stat'
 import dayjs from 'dayjs'
+import type { Ref } from 'vue'
 
 interface RyProductionBriefRow {
   id: number
@@ -30,9 +31,22 @@ const MERGE_COLUMN_INDEXES = [0, 1]
 const COMPANY_ORDER = ['钻井', '修井']
 const DEFAULT_DATE = dayjs().format('YYYY-MM-DD')
 
+const props = withDefaults(
+  defineProps<{
+    pageMode?: 'compact' | 'full'
+  }>(),
+  {
+    pageMode: 'compact'
+  }
+)
+
 const selectedDate = ref(DEFAULT_DATE)
 const loading = ref(false)
 const list = ref<RyProductionBriefRow[]>([])
+const kbScale = inject<Ref<number>>('ryKbScale', ref(1))
+const tableHeight = computed<number | string>(() =>
+  props.pageMode === 'full' ? '100%' : Math.round(TABLE_HEIGHT * kbScale.value)
+)
 
 const tableData = computed(() => {
   return [...list.value].sort((a, b) => {
@@ -163,8 +177,10 @@ onMounted(() => {
 </script>
 
 <template>
-  <div class="panel w-full h-[280px] flex flex-col mt-3">
-    <div class="panel-title h-9 flex items-center justify-between">
+  <div
+    class="panel device-list-panel production-brief-panel w-full flex flex-col"
+    :class="{ 'production-brief-panel--full': props.pageMode === 'full' }">
+    <div class="panel-title flex items-center justify-between">
       <div class="kb-panel-title-text flex items-center">
         <div class="icon-decorator">
           <span></span>
@@ -172,24 +188,25 @@ onMounted(() => {
         </div>
         生产简报
       </div>
-      <div class="w-120px! -translate-y-[4px]">
+      <div class="production-brief-panel__picker">
         <el-date-picker
           v-model="selectedDate"
           value-format="YYYY-MM-DD"
           type="date"
           placeholder="选择日期"
           :clearable="false"
-          class="w-120px!"
+          class="production-brief-panel__picker-input"
           @change="handleDateChange" />
       </div>
     </div>
-    <div class="flex-1 min-h-0 px-4 py-2">
+    <div class="device-list-panel__body flex-1 min-h-0">
       <el-table
         v-loading="loading"
         :data="tableData"
-        :height="TABLE_HEIGHT"
+        :height="tableHeight"
         :span-method="tableSpanMethod"
-        class="device-list-table production-brief-table">
+        class="device-list-table production-brief-table"
+        :class="{ 'production-brief-table--full': props.pageMode === 'full' }">
         <el-table-column prop="projectClassification" label="公司" min-width="72" align="center" />
         <el-table-column prop="projectName" label="项目" min-width="150" align="center" />
         <el-table-column prop="deptName" label="队伍" min-width="94" align="center" />
@@ -241,15 +258,61 @@ onMounted(() => {
 <style lang="scss" scoped>
 @import url('@/styles/kb.scss');
 
+.device-list-panel.production-brief-panel--full {
+  height: 100%;
+  margin-top: 0;
+}
+
+.production-brief-panel__picker {
+  display: flex;
+  width: calc(120px * var(--kb-scale, 1));
+  align-items: center;
+}
+
+.production-brief-panel__picker-input {
+  width: calc(120px * var(--kb-scale, 1)) !important;
+
+  :deep(.el-input__wrapper) {
+    min-height: calc(28px * var(--kb-scale, 1));
+    padding: 0 calc(10px * var(--kb-scale, 1));
+  }
+
+  :deep(.el-input__inner) {
+    font-size: calc(12px * var(--kb-scale, 1));
+  }
+
+  :deep(.el-input__prefix-inner),
+  :deep(.el-input__suffix-inner) {
+    font-size: calc(14px * var(--kb-scale, 1));
+  }
+}
+
 .production-brief-table {
+  width: 100%;
+
   :deep(.el-table__header-wrapper th.el-table__cell) {
-    font-size: 16px;
+    font-size: calc(16px * var(--kb-scale, 1));
     line-height: 1.2;
   }
 
   :deep(.el-table__body td.el-table__cell) {
-    padding: 7px 0;
-    font-size: 14px;
+    padding: calc(7px * var(--kb-scale, 1)) 0;
+    font-size: calc(14px * var(--kb-scale, 1));
+  }
+}
+
+.production-brief-table--full {
+  :deep(.el-scrollbar__view) {
+    display: block;
+    height: 100%;
+  }
+
+  :deep(.el-table__body) {
+    height: 100%;
+  }
+
+  :deep(.el-table__body tbody) {
+    height: 100%;
   }
 }
 </style>

+ 35 - 7
src/views/pms/stat/rykb/rydeviceList.vue

@@ -2,6 +2,7 @@
 import { IotStatApi } from '@/api/pms/stat'
 import { rangeShortcuts } from '@/utils/formatTime'
 import dayjs from 'dayjs'
+import type { Ref } from 'vue'
 
 interface RhDeviceListRow {
   projectDeptId: number
@@ -34,8 +35,10 @@ const teamDialogVisible = ref(false)
 const teamLoading = ref(false)
 const currentProjectDeptName = ref('')
 const teamList = ref<RhTeamRateRow[]>([])
+const kbScale = inject<Ref<number>>('ryKbScale', ref(1))
 
 const tableData = computed(() => list.value)
+const tableHeight = computed(() => Math.round(TABLE_HEIGHT * kbScale.value))
 
 function formatRate(value?: number | null) {
   return `${(Number(value ?? 0) * 100).toFixed(2)}%`
@@ -95,7 +98,7 @@ onMounted(() => {
 
 <template>
   <div class="panel w-full h-full flex flex-col">
-    <div class="panel-title h-9 flex items-center justify-between">
+    <div class="panel-title flex items-center justify-between">
       <div class="kb-panel-title-text flex items-center">
         <div class="icon-decorator">
           <span></span>
@@ -103,7 +106,7 @@ onMounted(() => {
         </div>
         设备利用率
       </div>
-      <div class="w-220px! -translate-y-[4px]">
+      <div class="ry-device-list-panel__picker">
         <el-date-picker
           v-model="createTime"
           value-format="YYYY-MM-DD HH:mm:ss"
@@ -112,15 +115,15 @@ onMounted(() => {
           end-placeholder="结束日期"
           :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
           :clearable="false"
-          class="w-220px!"
+          class="ry-device-list-panel__picker-input"
           @change="handleDateChange" />
       </div>
     </div>
     <!-- v-loading="loading" -->
-    <div class="flex-1 min-h-0 px-4 py-2">
+    <div class="device-list-panel__body flex-1 min-h-0">
       <el-table
         :data="tableData"
-        :height="TABLE_HEIGHT"
+        :height="tableHeight"
         class="device-list-table"
         @row-click="handleRowClick">
         <el-table-column prop="projectDeptName" label="项目部" min-width="150" align="center" />
@@ -180,14 +183,39 @@ onMounted(() => {
 <style lang="scss" scoped>
 @import url('@/styles/kb.scss');
 
+.ry-device-list-panel__picker {
+  display: flex;
+  width: calc(220px * var(--kb-scale, 1));
+  align-items: center;
+}
+
+.ry-device-list-panel__picker-input {
+  width: calc(220px * var(--kb-scale, 1)) !important;
+
+  :deep(.el-input__wrapper) {
+    min-height: calc(28px * var(--kb-scale, 1));
+    padding: 0 calc(10px * var(--kb-scale, 1));
+  }
+
+  :deep(.el-range-input),
+  :deep(.el-range-separator) {
+    font-size: calc(12px * var(--kb-scale, 1));
+  }
+
+  :deep(.el-range__icon),
+  :deep(.el-range__close-icon) {
+    font-size: calc(14px * var(--kb-scale, 1));
+  }
+}
+
 .device-list-table {
   :deep(.el-table__header-wrapper th.el-table__cell) {
-    font-size: 17px;
+    font-size: calc(17px * var(--kb-scale, 1));
     line-height: 1.25;
   }
 
   :deep(.el-table__body td.el-table__cell) {
-    font-size: 15px;
+    font-size: calc(15px * var(--kb-scale, 1));
   }
 }
 </style>

+ 3 - 1
src/views/pms/stat/rykb/rydeviceStatus.vue

@@ -108,17 +108,19 @@ 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 h-9">
+    <div class="panel-title">
       <div class="icon-decorator">
         <span></span>
         <span></span>

+ 3 - 1
src/views/pms/stat/rykb/rydeviceType.vue

@@ -152,17 +152,19 @@ 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 h-9">
+    <div class="panel-title">
       <div class="icon-decorator">
         <span></span>
         <span></span>

+ 3 - 1
src/views/pms/stat/rykb/ryorderTrend.vue

@@ -185,17 +185,19 @@ 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 h-9">
+    <div class="panel-title">
       <div class="icon-decorator">
         <span></span>
         <span></span>

+ 4 - 4
src/views/pms/stat/rykb/rysummary.vue

@@ -260,8 +260,8 @@ onMounted(() => {
 </script>
 
 <template>
-  <div class="panel w-full h-28 flex flex-col">
-    <div class="panel-title h-8">
+  <div class="panel summary-panel w-full flex flex-col">
+    <div class="panel-title summary-panel__title">
       <div class="icon-decorator">
         <span></span>
         <span></span>
@@ -269,11 +269,11 @@ onMounted(() => {
       工单情况
     </div>
 
-    <div class="grid grid-cols-10 gap-2.5 flex-1 px-2.5 py-1.5">
+    <div class="summary-panel__grid grid grid-cols-10 flex-1">
       <article
         v-for="card in summaryCards"
         :key="card.key"
-        class="summary-card relative flex h-full overflow-hidden rounded-md items-center gap-2 p-2"
+        class="summary-card relative flex h-full overflow-hidden rounded-md items-center"
         :style="{
           '--card-accent': card.accent,
           '--card-glow': card.glow

+ 19 - 18
src/views/pms/stat/rykb/safeday.vue

@@ -24,7 +24,7 @@ onMounted(() => {
 
 <template>
   <div class="panel flex flex-col">
-    <div class="panel-title h-9">
+    <div class="panel-title">
       <div class="icon-decorator">
         <span></span>
         <span></span>
@@ -61,7 +61,8 @@ onMounted(() => {
 .safe-day-panel {
   position: relative;
   display: flex;
-  padding: 22px 24px 18px;
+  padding: calc(22px * var(--kb-scale, 1)) calc(24px * var(--kb-scale, 1))
+    calc(18px * var(--kb-scale, 1));
   overflow: hidden;
   flex-direction: column;
 }
@@ -75,15 +76,15 @@ onMounted(() => {
 }
 
 .safe-day-panel__orbit--outer {
-  top: 32px;
-  width: 320px;
-  height: 320px;
+  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: 62px;
-  width: 240px;
-  height: 240px;
+  top: calc(62px * var(--kb-scale, 1));
+  width: calc(240px * var(--kb-scale, 1));
+  height: calc(240px * var(--kb-scale, 1));
   border-style: dashed;
 }
 
@@ -100,9 +101,9 @@ onMounted(() => {
 }
 
 .safe-day-panel__tag {
-  padding: 5px 12px;
+  padding: calc(5px * var(--kb-scale, 1)) calc(12px * var(--kb-scale, 1));
   font-family: YouSheBiaoTiHei, sans-serif;
-  font-size: 16px;
+  font-size: calc(16px * var(--kb-scale, 1));
   line-height: 1;
   letter-spacing: 1px;
   color: #1f5bb8;
@@ -114,7 +115,7 @@ onMounted(() => {
 
 .safe-day-panel__center {
   display: flex;
-  padding-top: 14px;
+  padding-top: calc(14px * var(--kb-scale, 1));
   margin-top: auto;
   margin-bottom: auto;
   flex-direction: column;
@@ -124,7 +125,7 @@ onMounted(() => {
 }
 
 .safe-day-panel__headline {
-  font-size: 20px;
+  font-size: calc(20px * var(--kb-scale, 1));
   font-weight: 600;
   letter-spacing: 1px;
   color: #24364f;
@@ -132,14 +133,14 @@ onMounted(() => {
 
 .safe-day-panel__value-row {
   display: flex;
-  margin-top: 10px;
+  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: 82px;
+  font-size: calc(82px * var(--kb-scale, 1));
   line-height: 0.9;
   letter-spacing: 2px;
   color: #1f5bb8;
@@ -147,16 +148,16 @@ onMounted(() => {
 }
 
 .safe-day-panel__unit {
-  padding-bottom: 10px;
+  padding-bottom: calc(10px * var(--kb-scale, 1));
   font-family: YouSheBiaoTiHei, sans-serif;
-  font-size: 30px;
+  font-size: calc(30px * var(--kb-scale, 1));
   line-height: 1;
   color: #f08c2e;
 }
 
 .safe-day-panel__subline {
-  margin-top: 10px;
-  font-size: 15px;
+  margin-top: calc(10px * var(--kb-scale, 1));
+  font-size: calc(15px * var(--kb-scale, 1));
   color: #6f85aa;
 }
 </style>

+ 3 - 1
src/views/pms/stat/rykb/xjwork.vue

@@ -197,17 +197,19 @@ 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 h-9">
+    <div class="panel-title">
       <div class="icon-decorator">
         <span></span>
         <span></span>

+ 7 - 7
src/views/pms/stat/rykb/zjStatsSwitch.vue

@@ -24,7 +24,7 @@ const activeTitle = computed(() =>
 
 <template>
   <div class="panel flex flex-col">
-    <div class="panel-title h-9 flex items-center justify-between">
+    <div class="panel-title flex items-center justify-between">
       <div class="kb-panel-title-text flex items-center">
         <div class="icon-decorator">
           <span></span>
@@ -50,15 +50,15 @@ const activeTitle = computed(() =>
   --el-segmented-bg-color: rgb(31 91 184 / 10%);
   --el-segmented-item-hover-bg-color: rgb(255 255 255 / 56%);
 
-  min-height: 26px;
-  padding: 2px;
+  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(-2px);
+  transform: translateY(calc(-2px * var(--kb-scale, 1)));
 
   :deep(.el-segmented__item) {
-    min-height: 22px;
-    padding: 0 8px;
-    font-size: 13px;
+    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;
   }

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

@@ -202,17 +202,19 @@ 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="embedded ? 'h-full flex flex-col' : 'panel flex flex-col'">
-    <div v-if="!embedded" class="panel-title h-9">
+    <div v-if="!embedded" class="panel-title">
       <div class="icon-decorator">
         <span></span>
         <span></span>

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

@@ -245,17 +245,19 @@ 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="embedded ? 'h-full flex flex-col' : 'panel flex flex-col'">
-    <div v-if="!embedded" class="panel-title h-9">
+    <div v-if="!embedded" class="panel-title">
       <div class="icon-decorator">
         <span></span>
         <span></span>