yanghao há 1 dia atrás
pai
commit
39adb71b19
2 ficheiros alterados com 413 adições e 264 exclusões
  1. 1 1
      .env.local
  2. 412 263
      src/views/pms/qhse/kanban/index.vue

+ 1 - 1
.env.local

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

+ 412 - 263
src/views/pms/qhse/kanban/index.vue

@@ -1,25 +1,21 @@
 <script lang="ts" setup>
 import * as echarts from 'echarts'
-import { kanbanApi } from '@/api/pms/qhse/index'
-import { IotDangerApi, IotMeasureCertApi, IotInstrumentApi } from '@/api/pms/qhse/index'
-import { useUserStore } from '@/store/modules/user'
-
-const userStore = useUserStore()
 import {
   AlarmClock,
   Checked,
+  Clock,
   CollectionTag,
   DataAnalysis,
   DocumentChecked,
   Files,
   Flag,
   Histogram,
-  Clock,
-  Opportunity,
   Odometer,
   Postcard,
   Warning
 } from '@element-plus/icons-vue'
+import { kanbanApi, IotDangerApi, IotInstrumentApi, IotMeasureCertApi } from '@/api/pms/qhse/index'
+import { useUserStore } from '@/store/modules/user'
 import {
   ANIMATION,
   CHART_RENDERER,
@@ -29,6 +25,8 @@ import {
   THEME,
   createTooltip
 } from '@/utils/kb'
+import { DICT_TYPE, getBoolDictOptions, getStrDictOptions } from '@/utils/dict'
+import { formatDate } from '@/utils/formatTime'
 
 defineOptions({
   name: 'PmsQhseKanban'
@@ -42,6 +40,10 @@ type PermitStat = {
 
 type SafeDayMap = Record<string, number>
 
+type SummaryTabValue = 'home' | 'certificate'
+
+const userStore = useUserStore()
+
 const wrapperRef = ref<HTMLDivElement>()
 const hazardChartRef = ref<HTMLDivElement>()
 const safeDayChartRef = ref<HTMLDivElement>()
@@ -54,14 +56,17 @@ let resizeRaf = 0
 let hazardChart: echarts.ECharts | null = null
 let safeDayChart: echarts.ECharts | null = null
 let socChart: echarts.ECharts | null = null
-const summaryTabs = [
-  { label: '首页', value: 'home' },
-  { label: '证书信息', value: 'report' }
-] as const
-const activeSummaryTab = ref<(typeof summaryTabs)[number]['value']>('home')
+
+const summaryTabs: Array<{ label: string; value: SummaryTabValue }> = [
+  { label: '首页看板', value: 'home' },
+  { label: '证书信息', value: 'certificate' }
+]
+const activeSummaryTab = ref<SummaryTabValue>('home')
 const summaryDate = ref('2026-06-02')
 
-const pageTitle = 'QHSE管理看板'
+const pageTitle = computed(() =>
+  activeSummaryTab.value === 'certificate' ? '证书信息看板' : 'QHSE管理看板'
+)
 
 const summaryCards = ref([
   {
@@ -88,11 +93,10 @@ const summaryCards = ref([
     glow: 'rgba(255, 90, 98, 0.26)',
     icon: Warning
   },
-
   {
     title: '特种人员持证率',
     value: 0,
-    note: 'Warn:3',
+    note: 'Warn: 0',
     accent: '#f2b800',
     glow: 'rgba(242, 184, 0, 0.22)',
     icon: Postcard
@@ -106,7 +110,7 @@ const summaryCards = ref([
     icon: DataAnalysis
   },
   {
-    title: '作业许可数量',
+    title: '作业许可数量',
     value: 0,
     note: '',
     accent: '#4f8dff',
@@ -128,13 +132,13 @@ const riskZones = ref([
   { title: '', desc: '危化库 / 试压区 / 配电房', color: '#ff5b61', value: 0 }
 ])
 
-const permitStats: PermitStat[] = [
+const permitStats = ref<PermitStat[]>([
   { label: '个人防护', value: 18, color: '#4f8dff' },
   { label: '规范操作', value: 26, color: '#43c7ca' },
   { label: '规范指挥', value: 12, color: '#ffb14a' },
   { label: '人员位置', value: 9, color: '#ff7a7a' },
   { label: '作业场所', value: 15, color: '#8d8cff' }
-]
+])
 
 const qualificationWarnings = ref([
   { label: '证件过期', value: 0, accent: '#ff7a7a' },
@@ -211,81 +215,54 @@ function updateScale() {
     if (!clientWidth || !clientHeight) return
 
     scale.value = Math.min(clientWidth / DESIGN_WIDTH, clientHeight / DESIGN_HEIGHT)
+
+    nextTick(() => {
+      resizeHazardChart()
+      resizeSafeDayChart()
+      resizeSocChart()
+    })
   })
 }
 
-const staticData = ref({})
+const staticData = ref<any>({})
+const safeDay = ref<SafeDayMap>({})
+const summaryPanel = ref<any>(null)
+const total = ref(0)
+const instrumentExpired = ref(0)
+
 async function getStatic() {
   const res = await IotDangerApi.getDangerStatistics(userStore.user.deptId)
-  staticData.value = res.classify
+  staticData.value = res.classify || []
 
-  riskZones.value.forEach((zone, index: number) => {
-    zone.value = staticData.value[index].count
-    zone.title = `${staticData.value[index].classify}区`
+  riskZones.value.forEach((zone, index) => {
+    const item = staticData.value[index]
+    if (!item) return
+    zone.value = item.count || 0
+    zone.title = `${item.classify}区`
   })
 }
 
-let expired = ref(0)
-let warn = ref(0)
 async function getCertStatic() {
   const res = await IotMeasureCertApi.getIotMeasureCertStatistics(userStore.user.deptId)
-  expired.value = res.expired
-  warn.value = res.warn
-
-  qualificationWarnings.value[0].value = expired.value
-  qualificationWarnings.value[1].value = warn.value
+  qualificationWarnings.value[0].value = res.expired || 0
+  qualificationWarnings.value[1].value = res.warn || 0
 }
 
-onMounted(() => {
-  supportsZoom.value = typeof CSS !== 'undefined' && CSS.supports?.('zoom', '1') === true
-  nextTick(updateScale)
-  resizeObserver = new ResizeObserver(updateScale)
-  if (wrapperRef.value) {
-    resizeObserver.observe(wrapperRef.value)
-  }
-  initHazardChart()
-  initSafeDayChart()
-  initSocChart()
-  window.addEventListener('resize', updateScale)
-  window.addEventListener('resize', resizeHazardChart)
-  window.addEventListener('resize', resizeSafeDayChart)
-  window.addEventListener('resize', resizeSocChart)
-})
-
-onUnmounted(() => {
-  resizeObserver?.disconnect()
-  window.removeEventListener('resize', updateScale)
-  window.removeEventListener('resize', resizeHazardChart)
-  window.removeEventListener('resize', resizeSafeDayChart)
-  window.removeEventListener('resize', resizeSocChart)
-  cancelAnimationFrame(resizeRaf)
-  destroyHazardChart()
-  destroySafeDayChart()
-  destroySocChart()
-})
-
-// 仪器
-let total = ref(0)
-let instrumentExpired = ref(0)
-const getList = async () => {
-  try {
-    const data = await IotInstrumentApi.getInstrumentList({
-      pageNo: 1,
-      pageSize: 10,
-      deptId: userStore.user.deptId
-    })
-    total.value = data.total
-
-    const res = await IotInstrumentApi.getInstrumentStatistics(userStore.user.deptId)
+async function getInstrumentOverview() {
+  const listData = await IotInstrumentApi.getInstrumentList({
+    pageNo: 1,
+    pageSize: 10,
+    deptId: userStore.user.deptId
+  })
+  total.value = listData.total || 0
 
-    instrumentExpired.value = res.expired
+  const statData = await IotInstrumentApi.getInstrumentStatistics(userStore.user.deptId)
+  instrumentExpired.value = statData.expired || 0
 
-    bottomCards.value[1].lines = [
-      `在用: ${total.value}台`,
-      `待检: ${instrumentExpired.value}台(重点关注)`
-    ]
-  } finally {
-  }
+  bottomCards.value[1].lines = [
+    `在用: ${total.value}台`,
+    `待检: ${instrumentExpired.value}台(重点关注)`
+  ]
 }
 
 function getHazardChartOption(): echarts.EChartsOption {
@@ -314,12 +291,8 @@ function getHazardChartOption(): echarts.EChartsOption {
     xAxis: {
       type: 'category',
       data: hazardBars.value.map((item) => item.label),
-      axisLine: {
-        show: false
-      },
-      axisTick: {
-        show: false
-      },
+      axisLine: { show: false },
+      axisTick: { show: false },
       axisLabel: {
         color: '#5b6f8f',
         fontSize: 16,
@@ -330,12 +303,8 @@ function getHazardChartOption(): echarts.EChartsOption {
     },
     yAxis: {
       type: 'value',
-      axisLine: {
-        show: false
-      },
-      axisTick: {
-        show: false
-      },
+      axisLine: { show: false },
+      axisTick: { show: false },
       axisLabel: {
         color: '#8a9bb5',
         fontSize: 13,
@@ -364,21 +333,13 @@ function getHazardChartOption(): echarts.EChartsOption {
           }
         })),
         barWidth: 70,
-
-        backgroundStyle: {
-          color: 'rgba(31, 91, 184, 0.06)',
-          borderRadius: 0
-        },
         label: {
           show: true,
           position: 'top',
           color: '#3c5f96',
           fontSize: 16,
           fontWeight: 700,
-          fontFamily: FONT_FAMILY,
-          formatter(params: any) {
-            return `${params.value}`
-          }
+          fontFamily: FONT_FAMILY
         },
         emphasis: {
           itemStyle: {
@@ -392,9 +353,7 @@ function getHazardChartOption(): echarts.EChartsOption {
 
 function initHazardChart() {
   if (!hazardChartRef.value) return
-  if (hazardChart) {
-    hazardChart.dispose()
-  }
+  hazardChart?.dispose()
   hazardChart = echarts.init(hazardChartRef.value, undefined, {
     renderer: CHART_RENDERER
   })
@@ -402,8 +361,7 @@ function initHazardChart() {
 }
 
 function updateHazardChart() {
-  if (!hazardChart) return
-  hazardChart.setOption(getHazardChartOption(), true)
+  hazardChart?.setOption(getHazardChartOption(), true)
 }
 
 function resizeHazardChart() {
@@ -411,16 +369,12 @@ function resizeHazardChart() {
 }
 
 function destroyHazardChart() {
-  if (hazardChart) {
-    hazardChart.dispose()
-    hazardChart = null
-  }
+  hazardChart?.dispose()
+  hazardChart = null
 }
 
 function getSafeDayEntries() {
-  const safeDayMap = (safeDay.value ?? {}) as SafeDayMap
-
-  return Object.entries(safeDayMap)
+  return Object.entries(safeDay.value || {})
     .map(([label, value]) => ({
       label,
       value: Number(value) || 0
@@ -455,12 +409,8 @@ function getSafeDayChartOption(): echarts.EChartsOption {
     }),
     xAxis: {
       type: 'value',
-      axisLine: {
-        show: false
-      },
-      axisTick: {
-        show: false
-      },
+      axisLine: { show: false },
+      axisTick: { show: false },
       axisLabel: {
         color: '#263854',
         fontSize: 12,
@@ -476,12 +426,8 @@ function getSafeDayChartOption(): echarts.EChartsOption {
     yAxis: {
       type: 'category',
       data: entries.map((item) => item.label),
-      axisLine: {
-        show: false
-      },
-      axisTick: {
-        show: false
-      },
+      axisLine: { show: false },
+      axisTick: { show: false },
       axisLabel: {
         color: '#16263d',
         fontSize: 14,
@@ -505,12 +451,6 @@ function getSafeDayChartOption(): echarts.EChartsOption {
             { offset: 0, color: '#78a0ec' },
             { offset: 1, color: '#6a90dd' }
           ])
-        },
-        emphasis: {
-          itemStyle: {
-            shadowBlur: 16,
-            shadowColor: 'rgba(106, 144, 221, 0.34)'
-          }
         }
       }
     ]
@@ -519,9 +459,7 @@ function getSafeDayChartOption(): echarts.EChartsOption {
 
 function initSafeDayChart() {
   if (!safeDayChartRef.value) return
-  if (safeDayChart) {
-    safeDayChart.dispose()
-  }
+  safeDayChart?.dispose()
   safeDayChart = echarts.init(safeDayChartRef.value, undefined, {
     renderer: CHART_RENDERER
   })
@@ -529,8 +467,7 @@ function initSafeDayChart() {
 }
 
 function updateSafeDayChart() {
-  if (!safeDayChart) return
-  safeDayChart.setOption(getSafeDayChartOption(), true)
+  safeDayChart?.setOption(getSafeDayChartOption(), true)
 }
 
 function resizeSafeDayChart() {
@@ -538,10 +475,8 @@ function resizeSafeDayChart() {
 }
 
 function destroySafeDayChart() {
-  if (safeDayChart) {
-    safeDayChart.dispose()
-    safeDayChart = null
-  }
+  safeDayChart?.dispose()
+  safeDayChart = null
 }
 
 function getSocChartOption(): echarts.EChartsOption {
@@ -594,27 +529,22 @@ function getSocChartOption(): echarts.EChartsOption {
           length: 10,
           length2: 8
         },
-        data: permitStats.map((item) => ({
+        data: permitStats.value.map((item) => ({
           name: item.label,
           value: item.value,
           itemStyle: {
             color: item.color
           }
-        })),
-        emphasis: {
-          scale: true,
-          scaleSize: 6
-        }
+        }))
       }
     ],
     graphic: [
       {
         type: 'text',
-        left: '28.8%',
+        left: '29%',
         top: '43%',
         style: {
           text: 'SOC卡',
-
           fill: '#6b7f9c',
           fontSize: 18,
           fontWeight: 700,
@@ -627,8 +557,7 @@ function getSocChartOption(): echarts.EChartsOption {
         top: '50%',
         textAlign: 'center',
         style: {
-          text: `${permitStats.reduce((sum, item) => sum + item.value, 0)}`,
-
+          text: `${permitStats.value.reduce((sum, item) => sum + item.value, 0)}`,
           fill: '#114a9b',
           fontSize: 30,
           fontWeight: 700,
@@ -641,9 +570,7 @@ function getSocChartOption(): echarts.EChartsOption {
 
 function initSocChart() {
   if (!socChartRef.value) return
-  if (socChart) {
-    socChart.dispose()
-  }
+  socChart?.dispose()
   socChart = echarts.init(socChartRef.value, undefined, {
     renderer: CHART_RENDERER
   })
@@ -655,71 +582,147 @@ function resizeSocChart() {
 }
 
 function destroySocChart() {
-  if (socChart) {
-    socChart.dispose()
-    socChart = null
-  }
-}
-
-const getKanban = async () => {
-  const data = await kanbanApi.getKanban()
-  return data
+  socChart?.dispose()
+  socChart = null
 }
 
 function formatPercent(numerator: number, denominator: number) {
   if (!denominator || Number.isNaN(denominator)) {
     return '0%'
   }
-
   return `${((numerator / denominator) * 100).toFixed(1)}%`
 }
 
-const summaryPanel = ref<any>(null)
-const safeDay = ref<SafeDayMap>({})
-onMounted(async () => {
-  summaryPanel.value = await getKanban()
-
-  // summaryCards.value[0].value = summaryPanel.value.danger
-  // summaryCards.value[1].value = summaryPanel.value.monthHazard
-  // summaryCards.value[2].value = formatPercent(
-  //   summaryPanel.value.totalHazard - summaryPanel.value.todoHazard,
-  //   summaryPanel.value.totalHazard
-  // )
-  // summaryCards.value[3].value = summaryPanel.value.ptwCount
+async function loadHomeBoard() {
+  summaryPanel.value = await kanbanApi.getKanban()
+
   summaryCards.value[3].value = formatPercent(
     summaryPanel.value.totdalCert - summaryPanel.value.expiredCert,
     summaryPanel.value.totdalCert
   )
+  summaryCards.value[3].note = `Warn: ${summaryPanel.value.warnCert || 0}`
 
-  summaryCards.value[3].note = `Warn: ${summaryPanel.value.warnCert}`
+  hazardBars.value[0].value = summaryPanel.value.totalHazard || 0
+  hazardBars.value[1].value =
+    (summaryPanel.value.totalHazard || 0) - (summaryPanel.value.todoHazard || 0)
+  hazardBars.value[2].value = summaryPanel.value.todoHazard || 0
 
-  hazardBars.value[0].value = summaryPanel.value.totalHazard
-  hazardBars.value[1].value = summaryPanel.value.totalHazard - summaryPanel.value.todoHazard
-  hazardBars.value[2].value = summaryPanel.value.todoHazard
+  safeDay.value = (await kanbanApi.getSafeDay(userStore.getUser.deptId)) || {}
 
-  try {
-    safeDay.value = (await kanbanApi.getSafeDay(userStore.getUser.deptId)) || {}
-  } catch (error) {
-    console.log(error)
+  await Promise.all([getStatic(), getCertStatic(), getInstrumentOverview()])
+
+  nextTick(() => {
+    updateHazardChart()
+    updateSafeDayChart()
+    resizeHazardChart()
+    resizeSafeDayChart()
+    resizeSocChart()
+  })
+}
+
+const certLoading = ref(false)
+const certTableData = ref<any[]>([])
+const certTotal = ref(0)
+const certQueryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  type: undefined as string | undefined,
+  classify: undefined as string | undefined,
+  userName: '',
+  expired: undefined as string | undefined
+})
+
+function formatDateCorrectly(timestamp: any) {
+  if (!timestamp) return ''
+  let time = Number(timestamp)
+  if (time < 10000000000) {
+    time = time * 1000
   }
+  return formatDate(time).substring(0, 10)
+}
 
-  try {
-    getStatic()
-    getCertStatic()
-  } catch (error) {
-    console.log(error)
+function getCertificateTypeText(type: string) {
+  const map: Record<string, string> = {
+    personal: '个人证书',
+    organization: '组织证书',
+    other: '其他'
   }
+  return map[type] || type
+}
 
+async function getCertificateBoardList() {
+  certLoading.value = true
   try {
-    await getList()
-  } catch (error) {
-    console.log(error)
+    const data = await IotMeasureCertApi.getIotMeasureCertPage({
+      ...certQueryParams,
+      deptId: userStore.user.deptId,
+      type: 'organization'
+    })
+    certTableData.value = data.list || []
+    certTotal.value = data.total || 0
+  } finally {
+    certLoading.value = false
   }
+}
 
-  nextTick(() => {
-    updateHazardChart()
-    updateSafeDayChart()
-  })
+function handleCertificateQuery() {
+  certQueryParams.pageNo = 1
+  getCertificateBoardList()
+}
+
+function resetCertificateQuery() {
+  certQueryParams.pageNo = 1
+  certQueryParams.pageSize = 10
+  certQueryParams.type = undefined
+  certQueryParams.classify = undefined
+  certQueryParams.userName = ''
+  certQueryParams.expired = undefined
+  getCertificateBoardList()
+}
+
+watch(
+  () => activeSummaryTab.value,
+  async (value) => {
+    if (value === 'certificate') {
+      await getCertificateBoardList()
+    } else {
+      nextTick(() => {
+        resizeHazardChart()
+        resizeSafeDayChart()
+        resizeSocChart()
+      })
+    }
+  }
+)
+
+onMounted(async () => {
+  supportsZoom.value = typeof CSS !== 'undefined' && CSS.supports?.('zoom', '1') === true
+  nextTick(updateScale)
+  resizeObserver = new ResizeObserver(updateScale)
+  if (wrapperRef.value) {
+    resizeObserver.observe(wrapperRef.value)
+  }
+  initHazardChart()
+  initSafeDayChart()
+  initSocChart()
+  window.addEventListener('resize', updateScale)
+  window.addEventListener('resize', resizeHazardChart)
+  window.addEventListener('resize', resizeSafeDayChart)
+  window.addEventListener('resize', resizeSocChart)
+
+  await loadHomeBoard()
+})
+
+onUnmounted(() => {
+  resizeObserver?.disconnect()
+  window.removeEventListener('resize', updateScale)
+  window.removeEventListener('resize', resizeHazardChart)
+  window.removeEventListener('resize', resizeSafeDayChart)
+  window.removeEventListener('resize', resizeSocChart)
+  cancelAnimationFrame(resizeRaf)
+  destroyHazardChart()
+  destroySafeDayChart()
+  destroySocChart()
 })
 </script>
 
@@ -728,6 +731,7 @@ onMounted(async () => {
     <div class="mx-a overflow-hidden" :style="targetWrapperStyle">
       <div id="qhse-kanban" class="bg qhse-board" :style="targetAreaStyle">
         <header class="header">{{ pageTitle }}</header>
+
         <div class="summary-toolbar">
           <div class="summary-toolbar__tabs">
             <button
@@ -740,7 +744,8 @@ onMounted(async () => {
               {{ tab.label }}
             </button>
           </div>
-          <div class="summary-toolbar__date">
+
+          <div v-if="activeSummaryTab === 'home'" class="summary-toolbar__date">
             <span class="summary-toolbar__date-label">日期:</span>
             <el-date-picker
               v-model="summaryDate"
@@ -752,7 +757,8 @@ onMounted(async () => {
               class="summary-toolbar__picker" />
           </div>
         </div>
-        <div class="board-body">
+
+        <div v-if="activeSummaryTab === 'home'" class="board-body">
           <section class="panel summary-panel kb-stage-card kb-stage-card--1">
             <div class="panel-title">
               <span class="icon-decorator"><span></span><span></span></span>
@@ -780,9 +786,9 @@ onMounted(async () => {
                   <div class="summary-card__label">{{ card.title }}</div>
                   <div class="summary-tile__meta">
                     <span class="summary-tile__value">{{ card.value }}</span>
-                    <span class="summary-tile__note" :style="{ color: card.accent }">{{
-                      card.note
-                    }}</span>
+                    <span class="summary-tile__note" :style="{ color: card.accent }">
+                      {{ card.note }}
+                    </span>
                   </div>
                 </div>
               </article>
@@ -801,7 +807,6 @@ onMounted(async () => {
                     <div class="risk-card__title">
                       <span class="risk-card__dot" :style="{ background: zone.color }"></span>
                       <span :style="{ color: zone.color }">{{ zone.title }}</span>
-                      <!-- <span class="risk-card__count">{{ zone.value }}</span> -->
                       <CountTo
                         :duration="2600"
                         :end-val="zone.value"
@@ -813,6 +818,7 @@ onMounted(async () => {
                 </div>
               </section>
             </div>
+
             <div class="left-column">
               <section class="panel board-panel kb-stage-card kb-stage-card--2">
                 <div class="panel-title">
@@ -863,9 +869,7 @@ onMounted(async () => {
                           :end-val="item.value"
                           :start-val="0"
                           :style="{ color: item.accent }" />
-
-                        <span v-if="item.label === '证件过期'" class="pl-2">人</span>
-                        <span v-else>人(需复审)</span>
+                        <span class="pl-2">人</span>
                       </strong>
                     </div>
                   </div>
@@ -896,33 +900,133 @@ onMounted(async () => {
                 <div class="bottom-card__title">{{ card.title }}</div>
               </div>
               <div class="bottom-card__content">
-                <template v-if="card.title !== '安全检测'">
-                  <p v-for="line in card.lines" :key="line">{{ line }}</p>
-                </template>
-                <template v-else>
-                  <p>
-                    <span>在用:</span>
-                    <CountTo
-                      :duration="2600"
-                      :end-val="total"
-                      :start-val="0"
-                      :style="{ color: card.accent }" />
-                    <span class="pl-2">台</span>
-                  </p>
-                  <p>
-                    <span>待检:</span>
-                    <CountTo
-                      :duration="2600"
-                      :end-val="instrumentExpired"
-                      :start-val="0"
-                      :style="{ color: '#ff7a7a' }" />
-                    <span class="pl-2">台(重点关注)</span>
-                  </p>
-                </template>
+                <p v-for="line in card.lines" :key="line">{{ line }}</p>
               </div>
             </article>
           </section>
         </div>
+
+        <div v-else class="certificate-board">
+          <section class="panel certificate-filter-panel">
+            <div class="certificate-filters">
+              <!-- <div class="certificate-filter-item">
+                <span class="certificate-filter-item__label">证书类型</span>
+                <el-select
+                  v-model="certQueryParams.type"
+                  clearable
+                  placeholder="请选择证书类型"
+                  class="certificate-filter-item__control">
+                  <el-option label="个人证书" value="personal" />
+                  <el-option label="组织证书" value="organization" />
+                  <el-option label="其他" value="other" />
+                </el-select>
+              </div> -->
+
+              <div class="certificate-filter-item">
+                <span class="certificate-filter-item__label">证书类别</span>
+                <el-select
+                  v-model="certQueryParams.classify"
+                  clearable
+                  placeholder="证书类别"
+                  class="certificate-filter-item__control">
+                  <el-option
+                    v-for="dict in getStrDictOptions(DICT_TYPE.PERSON_CERT).concat(
+                      getStrDictOptions(DICT_TYPE.ORG_CERT)
+                    )"
+                    :key="dict.value"
+                    :label="dict.label"
+                    :value="dict.value" />
+                </el-select>
+              </div>
+
+              <div class="certificate-filter-item">
+                <span class="certificate-filter-item__label">所属人</span>
+                <el-input
+                  v-model="certQueryParams.userName"
+                  placeholder="输入所属人"
+                  class="certificate-filter-item__control" />
+              </div>
+
+              <div class="certificate-filter-item">
+                <span class="certificate-filter-item__label">是否过期</span>
+                <el-select
+                  v-model="certQueryParams.expired"
+                  clearable
+                  placeholder="请选择是否过期"
+                  class="certificate-filter-item__control">
+                  <el-option
+                    v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+                    :key="dict.value"
+                    :label="dict.label"
+                    :value="dict.value" />
+                </el-select>
+              </div>
+
+              <div class="certificate-filter-actions">
+                <el-button type="primary" @click="handleCertificateQuery">查询</el-button>
+                <el-button @click="resetCertificateQuery">重置</el-button>
+              </div>
+            </div>
+          </section>
+
+          <section class="panel certificate-table-panel">
+            <el-table
+              :loading="certLoading"
+              :data="certTableData"
+              height="100%"
+              class="certificate-table">
+              <el-table-column label="序号" width="80" align="center">
+                <template #default="scope">
+                  {{ (certQueryParams.pageNo - 1) * certQueryParams.pageSize + scope.$index + 1 }}
+                </template>
+              </el-table-column>
+
+              <el-table-column label="证书类型" align="center" min-width="120">
+                <template #default="scope">
+                  {{ getCertificateTypeText(scope.row.type) }}
+                </template>
+              </el-table-column>
+
+              <el-table-column label="证书类别" align="center" min-width="140">
+                <template #default="scope">
+                  <dict-tag
+                    v-if="scope.row.type === 'organization'"
+                    :type="DICT_TYPE.ORG_CERT"
+                    :value="scope.row.classify" />
+                  <dict-tag v-else :type="DICT_TYPE.PERSON_CERT" :value="scope.row.classify" />
+                </template>
+              </el-table-column>
+
+              <el-table-column label="证书名称" prop="certName" align="center" min-width="160" />
+              <el-table-column label="所属人" prop="userName" align="center" min-width="100" />
+              <el-table-column label="所在部门" prop="deptName" align="center" min-width="140" />
+              <el-table-column label="颁发机构" prop="certOrg" align="center" min-width="220" />
+              <el-table-column
+                label="证书标准"
+                prop="certStandard"
+                align="center"
+                min-width="140" />
+              <el-table-column label="颁发时间" align="center" min-width="120">
+                <template #default="scope">
+                  {{ formatDateCorrectly(scope.row.certIssue) }}
+                </template>
+              </el-table-column>
+              <el-table-column label="有效期" align="center" min-width="120">
+                <template #default="scope">
+                  {{ formatDateCorrectly(scope.row.certExpire) }}
+                </template>
+              </el-table-column>
+            </el-table>
+
+            <div class="certificate-pagination">
+              <Pagination
+                :total="certTotal"
+                v-model:page="certQueryParams.pageNo"
+                v-model:limit="certQueryParams.pageSize"
+                @pagination="getCertificateBoardList" />
+            </div>
+          </section>
+        </div>
       </div>
     </div>
   </div>
@@ -939,18 +1043,11 @@ onMounted(async () => {
   padding: 18px 18px 24px;
 }
 
-.summary-panel {
-  padding-bottom: 22px;
-  padding-left: 10px;
-  padding-right: 10px;
-  height: 210px;
-}
-
 .summary-toolbar {
   display: flex;
   align-items: center;
   justify-content: space-between;
-  padding: 8px 14px 4px;
+  padding: 10px 18px 6px;
 }
 
 .summary-toolbar__tabs {
@@ -960,12 +1057,11 @@ onMounted(async () => {
 }
 
 .summary-toolbar__tab {
-  min-width: 112px;
-  height: 38px;
-  padding: 0 20px;
+  min-width: 120px;
+  height: 40px;
+  padding: 0 22px;
   font-family: YouSheBiaoTiHei, sans-serif;
   font-size: 17px;
-  line-height: 1;
   color: #2b76e9;
   background: linear-gradient(180deg, rgb(255 255 255 / 92%) 0%, rgb(228 239 255 / 90%) 100%);
   border: 1px solid rgb(126 174 244 / 44%);
@@ -973,13 +1069,12 @@ onMounted(async () => {
   box-shadow:
     inset 0 1px 0 rgb(255 255 255 / 96%),
     0 8px 14px rgb(63 103 171 / 8%);
+  cursor: pointer;
   transition:
     transform 0.2s ease,
     box-shadow 0.2s ease,
     background 0.2s ease,
     color 0.2s ease;
-
-  cursor: pointer;
 }
 
 .summary-toolbar__tab.is-active {
@@ -1021,15 +1116,9 @@ onMounted(async () => {
     0 8px 14px rgb(63 103 171 / 6%);
 }
 
-.summary-toolbar__picker :deep(.el-input__inner) {
-  font-size: 16px;
-  font-weight: 700;
-  color: #3d5679;
-}
-
-.summary-toolbar__picker :deep(.el-input__prefix),
-.summary-toolbar__picker :deep(.el-input__suffix) {
-  color: #6c86a8;
+.summary-panel {
+  padding: 0 10px 22px;
+  height: 210px;
 }
 
 .summary-grid {
@@ -1088,14 +1177,14 @@ onMounted(async () => {
   gap: 24px;
 }
 
-.center-column {
-  display: block;
-}
-
 .board-panel {
   min-height: 258px;
 }
 
+.board-panel.kb-stage-card:hover {
+  transform: none;
+}
+
 .board-panel--center {
   min-height: 540px;
 }
@@ -1104,17 +1193,16 @@ onMounted(async () => {
   margin-top: 18px;
 }
 
-.chart-panel--bars {
-  height: 176px;
-}
-
 .chart-panel--echart {
   height: 188px;
 }
 
 .safe-day-chart-panel {
   height: 218px;
-  // margin-top: 18px;
+}
+
+.soc-chart-panel {
+  height: 220px;
 }
 
 .panel-title--center {
@@ -1169,11 +1257,6 @@ onMounted(async () => {
   line-height: 1.55;
 }
 
-.soc-chart-panel {
-  height: 220px;
-  // margin-top: 10px;
-}
-
 .qualification-panel {
   display: grid;
   margin-top: 26px;
@@ -1227,7 +1310,7 @@ onMounted(async () => {
 
 .bottom-card {
   min-height: 180px;
-  padding: 15px 22px 0px;
+  padding: 15px 22px 0;
   overflow: hidden;
 }
 
@@ -1262,4 +1345,70 @@ onMounted(async () => {
   color: #5d718e;
   line-height: 1.7;
 }
+
+.certificate-board {
+  padding: 16px 18px 24px;
+}
+
+.certificate-filter-panel {
+  padding: 18px 20px 12px;
+}
+
+.certificate-filters {
+  display: grid;
+  grid-template-columns: repeat(4, minmax(0, 1fr)) auto;
+  gap: 18px 20px;
+  align-items: end;
+}
+
+.certificate-filter-item {
+  display: flex;
+  align-items: center;
+  gap: 14px;
+}
+
+.certificate-filter-item__label {
+  flex-shrink: 0;
+  font-size: 16px;
+  font-weight: 700;
+  color: #4f6583;
+}
+
+.certificate-filter-item__control {
+  flex: 1;
+}
+
+.certificate-filter-item__control :deep(.el-input__wrapper) {
+  height: 42px !important; //
+  border-radius: 12px;
+}
+
+.certificate-filter-actions {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  justify-content: flex-end;
+}
+
+.certificate-table-panel {
+  margin-top: 18px;
+  padding: 18px 18px 12px;
+  height: 760px;
+  display: flex;
+  flex-direction: column;
+}
+
+.certificate-table {
+  flex: 1;
+}
+
+.certificate-table :deep(.el-table) {
+  --el-table-header-bg-color: rgba(248, 251, 255, 0.95);
+  --el-table-row-hover-bg-color: rgba(79, 141, 255, 0.06);
+  border-radius: 14px;
+}
+
+.certificate-pagination {
+  padding-top: 14px;
+}
 </style>