Просмотр исходного кода

Merge branch 'qhse_person' of shuzhihua/pms-iot-vue into master

yanghao 5 дней назад
Родитель
Сommit
3b8ba2b6a9

+ 1 - 1
.env.local

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

+ 10 - 0
src/api/pms/fiveconstructiondailyreport/index.ts

@@ -12,5 +12,15 @@ export const FiveIotProjectDailyReportApi = {
   // 填报项目日报
   updateIotProjectDailyReport: async (data) => {
     return await request.put({ url: '/pms/iot-five-daily-report/update', data })
+  },
+
+  // 日报汇总统计卡片
+  getIotProjectDailyReportSummary: async (params) => {
+    return await request.get({ url: '/pms/iot-five-daily-report/totalWorkload', params })
+  },
+
+  // 获取项目日报分页
+  getProjectDailyReportSummaryPage: async (params) => {
+    return await request.get({ url: '/pms/iot-five-daily-report/summaryStatistics', params })
   }
 }

+ 310 - 0
src/views/iot/commonThing/CommonThingModelForm.vue

@@ -0,0 +1,310 @@
+<template>
+  <Dialog v-model="dialogVisible" :title="dialogTitle" width="900px">
+    <el-form
+      ref="formRef"
+      v-loading="formLoading"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px">
+      <el-form-item label="功能标识" prop="identifier">
+        <el-input v-model="formData.identifier" placeholder="请输入功能标识" />
+      </el-form-item>
+      <el-form-item label="功能名称" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入功能名称" />
+      </el-form-item>
+      <el-form-item label="功能描述" prop="description">
+        <el-input
+          v-model="formData.description"
+          type="textarea"
+          :rows="3"
+          :maxlength="200"
+          placeholder="请输入功能描述" />
+      </el-form-item>
+      <el-form-item label="功能类型" prop="type">
+        <el-radio-group v-model="formData.type">
+          <el-radio-button
+            v-for="dict in getIntDictOptions(DICT_TYPE.IOT_THING_MODEL_TYPE)"
+            :key="dict.value"
+            :value="dict.value">
+            {{ dict.label }}
+          </el-radio-button>
+        </el-radio-group>
+      </el-form-item>
+
+      <el-form-item v-if="formData.type === 1" label="属性" prop="property">
+        <div class="json-editor-field">
+          <div class="json-editor-toolbar">
+            <span class="json-editor-toolbar__tip">请输入属性 JSON 数据</span>
+            <div class="json-editor-toolbar__actions">
+              <el-button text type="primary" @click="formatJsonField('property')">
+                美化 JSON
+              </el-button>
+              <el-button text @click="compressJsonField('property')">压缩 JSON</el-button>
+            </div>
+          </div>
+          <v-ace-editor
+            v-model:value="formData.property"
+            lang="json"
+            theme="monokai"
+            class="json-editor"
+            :options="editorOptions" />
+        </div>
+      </el-form-item>
+
+      <el-form-item v-if="formData.type === 3" label="事件" prop="event">
+        <div class="json-editor-field">
+          <div class="json-editor-toolbar">
+            <span class="json-editor-toolbar__tip">请输入事件 JSON 数据</span>
+            <div class="json-editor-toolbar__actions">
+              <el-button text type="primary" @click="formatJsonField('event')">美化 JSON</el-button>
+              <el-button text @click="compressJsonField('event')">压缩 JSON</el-button>
+            </div>
+          </div>
+          <v-ace-editor
+            v-model:value="formData.event"
+            lang="json"
+            theme="monokai"
+            class="json-editor"
+            :options="editorOptions" />
+        </div>
+      </el-form-item>
+
+      <el-form-item v-if="formData.type === 2" label="服务" prop="service">
+        <div class="json-editor-field">
+          <div class="json-editor-toolbar">
+            <span class="json-editor-toolbar__tip">请输入服务 JSON 数据</span>
+            <div class="json-editor-toolbar__actions">
+              <el-button text type="primary" @click="formatJsonField('service')">
+                美化 JSON
+              </el-button>
+              <el-button text @click="compressJsonField('service')">压缩 JSON</el-button>
+            </div>
+          </div>
+          <v-ace-editor
+            v-model:value="formData.service"
+            lang="json"
+            theme="monokai"
+            class="json-editor"
+            :options="editorOptions" />
+        </div>
+      </el-form-item>
+    </el-form>
+
+    <template #footer>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确定</el-button>
+      <el-button @click="dialogVisible = false">取消</el-button>
+    </template>
+  </Dialog>
+</template>
+
+<script lang="ts" setup>
+import '@/components/mt-edit/ace-edit'
+import { VAceEditor } from 'vue3-ace-editor'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { ThingModelCommon, type CommonThingModelData } from '@/api/iot/thingmodel'
+
+defineOptions({ name: 'CommonThingModelForm' })
+
+const { t } = useI18n()
+const message = useMessage()
+
+const dialogVisible = ref(false)
+const dialogTitle = ref('')
+const formLoading = ref(false)
+const formType = ref('')
+const formRef = ref()
+const editorOptions = {
+  useWorker: true,
+  enableBasicAutocompletion: true,
+  enableSnippets: true,
+  enableLiveAutocompletion: true
+}
+
+const createDefaultFormData = (): CommonThingModelData => ({
+  identifier: '',
+  name: '',
+  description: '',
+  type: 1,
+  property: '',
+  event: '',
+  service: ''
+})
+
+const formData = ref<CommonThingModelData>(createDefaultFormData())
+
+const getJsonFieldByType = () => {
+  if (formData.value.type === 1) return 'property'
+  if (formData.value.type === 2) return 'service'
+  return 'event'
+}
+
+const tryFormatJson = (value?: string, indent = 2) => {
+  if (!value) return ''
+  const trimmed = value.trim()
+  if (!trimmed) return ''
+  return JSON.stringify(JSON.parse(trimmed), null, indent)
+}
+
+const formatJsonField = (field: 'property' | 'event' | 'service') => {
+  try {
+    formData.value[field] = tryFormatJson(formData.value[field], 2)
+    message.success('JSON 已美化')
+  } catch {
+    message.warning('当前内容不是合法 JSON,无法美化')
+  }
+}
+
+const compressJsonField = (field: 'property' | 'event' | 'service') => {
+  try {
+    formData.value[field] = tryFormatJson(formData.value[field], 0)
+    message.success('JSON 已压缩')
+  } catch {
+    message.warning('当前内容不是合法 JSON,无法压缩')
+  }
+}
+
+const validateJsonField = (field: 'property' | 'event' | 'service', label: string) => {
+  return (_rule: any, value: string, callback: (error?: Error) => void) => {
+    const currentType = formData.value.type
+    const fieldTypeMap = {
+      property: 1,
+      service: 2,
+      event: 3
+    }
+
+    if (currentType !== fieldTypeMap[field]) {
+      callback()
+      return
+    }
+
+    if (!value) {
+      callback(new Error(`${label}不能为空`))
+      return
+    }
+
+    try {
+      JSON.parse(value)
+      callback()
+    } catch {
+      callback(new Error(`${label}必须是合法的 JSON`))
+    }
+  }
+}
+
+const formRules = reactive({
+  identifier: [{ required: true, message: '功能标识不能为空', trigger: 'blur' }],
+  name: [{ required: true, message: '功能名称不能为空', trigger: 'blur' }],
+  type: [{ required: true, message: '功能类型不能为空', trigger: 'change' }]
+  // property: [{ validator: validateJsonField('property', '属性'), trigger: 'blur' }],
+  // event: [{ validator: validateJsonField('event', '事件'), trigger: 'blur' }],
+  // service: [{ validator: validateJsonField('service', '服务'), trigger: 'blur' }]
+})
+
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  if (!id) return
+
+  formLoading.value = true
+  try {
+    const data = await ThingModelCommon.getCommonThingModel(id)
+    formData.value = {
+      ...createDefaultFormData(),
+      ...data
+    }
+    const activeField = getJsonFieldByType()
+    if (formData.value[activeField]) {
+      formData.value[activeField] = tryFormatJson(formData.value[activeField], 2)
+    }
+  } finally {
+    formLoading.value = false
+  }
+}
+
+defineExpose({ open, close: () => (dialogVisible.value = false) })
+
+const emit = defineEmits(['success'])
+
+const submitForm = async () => {
+  await formRef.value.validate()
+  formLoading.value = true
+  try {
+    const payload: CommonThingModelData = {
+      ...formData.value,
+      property: formData.value.type === 1 ? formData.value.property : '',
+      service: formData.value.type === 2 ? formData.value.service : '',
+      event: formData.value.type === 3 ? formData.value.event : ''
+    }
+
+    if (formType.value === 'create') {
+      await ThingModelCommon.createCommonThingModel(payload)
+      message.success(t('common.createSuccess'))
+    } else {
+      await ThingModelCommon.updateCommonThingModel(payload)
+      message.success(t('common.updateSuccess'))
+    }
+
+    dialogVisible.value = false
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+const resetForm = () => {
+  formData.value = createDefaultFormData()
+  formRef.value?.resetFields()
+}
+
+watch(
+  () => formData.value.type,
+  () => {
+    const activeField = getJsonFieldByType()
+    if (formData.value[activeField]) {
+      try {
+        formData.value[activeField] = tryFormatJson(formData.value[activeField], 2)
+      } catch {
+        // Keep original input when JSON is incomplete during editing.
+      }
+    }
+  }
+)
+</script>
+
+<style scoped>
+.json-editor-field {
+  width: 100%;
+  border: 1px solid var(--el-border-color);
+  border-radius: 8px;
+  overflow: hidden;
+  background: #111827;
+}
+
+.json-editor-toolbar {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px;
+  padding: 10px 12px;
+  background: #1f2937;
+  border-bottom: 1px solid rgb(255 255 255 / 0.08);
+}
+
+.json-editor-toolbar__tip {
+  font-size: 12px;
+  color: rgb(255 255 255 / 0.72);
+}
+
+.json-editor-toolbar__actions {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.json-editor {
+  width: 100%;
+  height: 320px;
+}
+</style>

+ 43 - 74
src/views/iot/commonThing/index.vue

@@ -2,17 +2,23 @@
   <ContentWrap>
     <el-form
       ref="queryFormRef"
-      :model="queryParams"
       :inline="true"
+      :model="queryParams"
       class="-mb-15px"
-      label-width="80px">
-      <el-form-item label="功能标识" prop="identifier">
-        <el-input
-          v-model="queryParams.identifier"
+      label-width="68px">
+      <el-form-item label="功能类型" prop="type">
+        <el-select
+          v-model="queryParams.type"
           class="!w-150px"
           clearable
-          placeholder="请输入功能标识"
-          @keyup.enter="handleQuery" />
+          placeholder="请选择功能类型"
+          @change="handleQuery">
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.IOT_THING_MODEL_TYPE)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value" />
+        </el-select>
       </el-form-item>
       <el-form-item label="功能名称" prop="name">
         <el-input
@@ -22,21 +28,16 @@
           placeholder="请输入功能名称"
           @keyup.enter="handleQuery" />
       </el-form-item>
-      <el-form-item label="功能类型" prop="type">
-        <el-select
-          v-model="queryParams.type"
+      <el-form-item label="功能标识" prop="identifier">
+        <el-input
+          v-model="queryParams.identifier"
           class="!w-150px"
           clearable
-          placeholder="请选择功能类型">
-          <el-option
-            v-for="dict in getIntDictOptions(DICT_TYPE.IOT_THING_MODEL_TYPE)"
-            :key="dict.value"
-            :label="dict.label"
-            :value="dict.value" />
-        </el-select>
+          placeholder="请输入功能标识"
+          @keyup.enter="handleQuery" />
       </el-form-item>
       <el-form-item>
-        <el-button @click="handleQuery">
+        <el-button type="primary" @click="handleQuery">
           <Icon class="mr-5px" icon="ep:search" />
           搜索
         </el-button>
@@ -46,44 +47,45 @@
         </el-button>
         <el-button plain type="primary" @click="openForm('create')">
           <Icon class="mr-5px" icon="ep:plus" />
-          新增
+          新增功能
         </el-button>
       </el-form-item>
     </el-form>
   </ContentWrap>
 
   <ContentWrap>
-    <zm-table :loading="loading" :data="list">
-      <zm-table-column align="center" label="功能标识" prop="identifier" min-width="160" />
-      <zm-table-column align="center" label="功能名称" prop="name" min-width="160" />
-      <zm-table-column align="center" label="功能描述" prop="description" min-width="180" />
+    <zm-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
       <zm-table-column align="center" label="功能类型" prop="type" width="120">
         <template #default="{ row }">
           <dict-tag :type="DICT_TYPE.IOT_THING_MODEL_TYPE" :value="row.type" />
         </template>
       </zm-table-column>
-      <zm-table-column align="left" label="属性 JSON" min-width="220">
+      <zm-table-column align="center" label="功能名称" prop="name" min-width="180" />
+      <zm-table-column align="center" label="功能标识" prop="identifier" min-width="180" />
+      <zm-table-column align="center" label="描述" prop="description" min-width="220" />
+      <zm-table-column align="left" label="属性" min-width="240" show-overflow-tooltip>
         <template #default="{ row }">
-          <JsonPreview :value="row.property" />
+          {{ row.property || '-' }}
         </template>
       </zm-table-column>
-      <zm-table-column align="left" label="事件 JSON" min-width="220">
+      <zm-table-column align="left" label="事件" min-width="240" show-overflow-tooltip>
         <template #default="{ row }">
-          <JsonPreview :value="row.event" />
+          {{ row.event || '-' }}
         </template>
       </zm-table-column>
-      <zm-table-column align="left" label="服务 JSON" min-width="220">
+      <zm-table-column align="left" label="服务" min-width="240" show-overflow-tooltip>
         <template #default="{ row }">
-          <JsonPreview :value="row.service" />
+          {{ row.service || '-' }}
         </template>
       </zm-table-column>
-      <zm-table-column align="center" label="操作" fixed="right" width="140">
+      <zm-table-column align="center" label="操作" action width="160" fixed="right">
         <template #default="{ row }">
           <el-button link type="primary" @click="openForm('update', row.id)"> 编辑 </el-button>
           <el-button link type="danger" @click="handleDelete(row.id)"> 删除 </el-button>
         </template>
       </zm-table-column>
     </zm-table>
+
     <Pagination
       v-model:limit="queryParams.pageSize"
       v-model:page="queryParams.pageNo"
@@ -91,69 +93,39 @@
       @pagination="getList" />
   </ContentWrap>
 
-  <ThingModelForm ref="formRef" mode="common" @success="getList" />
+  <CommonThingModelForm ref="formRef" @success="getList" />
 </template>
 
-<script setup lang="ts">
-import { h } from 'vue'
-import { CommonThingModelData, ThingModelCommon } from '@/api/iot/thingmodel'
-import ThingModelForm from '@/views/iot/thingmodel/ThingModelFormCommon.vue'
+<script lang="ts" setup>
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { ThingModelCommon, type CommonThingModelData } from '@/api/iot/thingmodel'
+import CommonThingModelForm from './CommonThingModelForm.vue'
 
-defineOptions({ name: 'IoTCommonThingModel' })
+defineOptions({ name: 'CommonThingModel' })
 
 const { t } = useI18n()
 const message = useMessage()
 
 const loading = ref(false)
-const total = ref(0)
 const list = ref<CommonThingModelData[]>([])
+const total = ref(0)
 const queryFormRef = ref()
 const formRef = ref()
 
 const queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
-  identifier: '',
+  type: undefined as number | undefined,
   name: '',
-  type: undefined as number | undefined
+  identifier: ''
 })
 
-const formatJsonText = (value: unknown) => {
-  if (!value) {
-    return '-'
-  }
-  if (typeof value === 'string') {
-    return value
-  }
-  try {
-    return JSON.stringify(value, null, 2)
-  } catch {
-    return String(value)
-  }
-}
-
-const JsonPreview = (props: { value?: unknown }) =>
-  h(
-    'div',
-    {
-      style: {
-        whiteSpace: 'pre-wrap',
-        wordBreak: 'break-all',
-        lineHeight: '20px',
-        maxHeight: '120px',
-        overflow: 'auto'
-      }
-    },
-    formatJsonText(props.value)
-  )
-
 const getList = async () => {
   loading.value = true
   try {
     const data = await ThingModelCommon.getCommonThingModelPage(queryParams)
-    list.value = data.list
-    total.value = data.total
+    list.value = data?.list || []
+    total.value = data?.total || 0
   } finally {
     loading.value = false
   }
@@ -173,10 +145,7 @@ const openForm = (type: string, id?: number) => {
   formRef.value?.open(type, id)
 }
 
-const handleDelete = async (id?: number) => {
-  if (!id) {
-    return
-  }
+const handleDelete = async (id: number) => {
   try {
     await message.delConfirm()
     await ThingModelCommon.deleteCommonThingModel(id)

+ 0 - 215
src/views/iot/thingmodel/ThingModelFormCommon.vue

@@ -1,215 +0,0 @@
-<!-- 产品的物模型表单 -->
-<template>
-  <Dialog v-model="dialogVisible" :title="dialogTitle">
-    <el-form
-      ref="formRef"
-      v-loading="formLoading"
-      :model="formData"
-      :rules="ThingModelFormRules"
-      label-width="100px">
-      <el-form-item label="功能类型" prop="type">
-        <el-radio-group v-model="formData.type">
-          <el-radio-button
-            v-for="dict in getIntDictOptions(DICT_TYPE.IOT_THING_MODEL_TYPE)"
-            :key="dict.value"
-            :value="dict.value">
-            {{ dict.label }}
-          </el-radio-button>
-        </el-radio-group>
-      </el-form-item>
-      <el-form-item label="功能名称" prop="name">
-        <el-input v-model="formData.name" placeholder="请输入功能名称" />
-      </el-form-item>
-      <el-form-item label="标识符" prop="identifier">
-        <el-input v-model="formData.identifier" placeholder="请输入标识符" />
-      </el-form-item>
-      <!-- 属性配置 -->
-      <ThingModelProperty
-        v-if="formData.type === IoTThingModelTypeEnum.PROPERTY"
-        v-model="formData.property" />
-      <!-- 服务配置 -->
-      <ThingModelService
-        v-if="formData.type === IoTThingModelTypeEnum.SERVICE"
-        v-model="formData.service" />
-      <!-- 事件配置 -->
-      <ThingModelEvent
-        v-if="formData.type === IoTThingModelTypeEnum.EVENT"
-        v-model="formData.event" />
-      <el-form-item label="描述" prop="description">
-        <el-input
-          v-model="formData.description"
-          :maxlength="200"
-          :rows="3"
-          placeholder="请输入属性描述"
-          type="textarea" />
-      </el-form-item>
-    </el-form>
-
-    <template #footer>
-      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
-      <el-button @click="dialogVisible = false">取 消</el-button>
-    </template>
-  </Dialog>
-</template>
-
-<script lang="ts" setup>
-import { ProductVO } from '@/api/iot/product/product'
-import ThingModelProperty from './ThingModelProperty.vue'
-import ThingModelService from './ThingModelService.vue'
-import ThingModelEvent from './ThingModelEvent.vue'
-import { ThingModelCommon, ThingModelData, ThingModelFormRules } from '@/api/iot/thingmodel'
-import {
-  IOT_PROVIDE_KEY,
-  IoTDataSpecsDataTypeEnum,
-  IoTThingModelTypeEnum
-} from '@/views/iot/utils/constants'
-import { cloneDeep } from 'lodash-es'
-import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
-import { isEmpty } from '@/utils/is'
-
-/** IoT 物模型数据表单 */
-defineOptions({ name: 'IoTThingModelForm' })
-
-const product = inject<Ref<ProductVO>>(IOT_PROVIDE_KEY.PRODUCT) // 注入产品信息
-
-const { t } = useI18n() // 国际化
-const message = useMessage() // 消息弹窗
-
-const dialogVisible = ref(false) // 弹窗的是否展示
-const dialogTitle = ref('') // 弹窗的标题
-const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
-const formType = ref('') // 表单的类型:create - 新增;update - 修改
-const formData = ref<ThingModelData>({
-  type: IoTThingModelTypeEnum.PROPERTY,
-  dataType: IoTDataSpecsDataTypeEnum.INT,
-  property: {
-    dataType: IoTDataSpecsDataTypeEnum.INT,
-    dataSpecs: {
-      dataType: IoTDataSpecsDataTypeEnum.INT
-    }
-  },
-  service: {},
-  event: {}
-})
-
-const formRef = ref() // 表单 Ref
-
-/** 打开弹窗 */
-const open = async (type: string, id?: number) => {
-  dialogVisible.value = true
-  dialogTitle.value = t('action.' + type)
-  formType.value = type
-  resetForm()
-  if (id) {
-    formLoading.value = true
-    try {
-      formData.value = await ThingModelCommon.getCommonThingModel(id)
-      // 情况一:属性初始化
-      if (isEmpty(formData.value.property)) {
-        formData.value.dataType = IoTDataSpecsDataTypeEnum.INT
-        formData.value.property = {
-          dataType: IoTDataSpecsDataTypeEnum.INT,
-          dataSpecs: {
-            dataType: IoTDataSpecsDataTypeEnum.INT
-          }
-        }
-      }
-      // 情况二:服务初始化
-      if (isEmpty(formData.value.service)) {
-        formData.value.service = {}
-      }
-      // 情况三:事件初始化
-      if (isEmpty(formData.value.event)) {
-        formData.value.event = {}
-      }
-    } finally {
-      formLoading.value = false
-    }
-  }
-}
-defineExpose({ open, close: () => (dialogVisible.value = false) })
-
-/** 提交表单 */
-const emit = defineEmits(['success'])
-const submitForm = async () => {
-  await formRef.value.validate()
-  formLoading.value = true
-  try {
-    const data = cloneDeep(formData.value) as ThingModelData
-    // // 信息补全
-    // data.productId = product!.value.id
-    // data.productKey = product!.value.productKey
-    fillExtraAttributes(data)
-    if (formType.value === 'create') {
-      await ThingModelCommon.createCommonThingModel(data)
-      message.success(t('common.createSuccess'))
-    } else {
-      await ThingModelCommon.updateCommonThingModel(data)
-      message.success(t('common.updateSuccess'))
-    }
-    // 关闭弹窗
-    dialogVisible.value = false
-    emit('success')
-  } finally {
-    formLoading.value = false
-  }
-}
-
-/** 填写额外的属性(处理不同类型的情况) */
-const fillExtraAttributes = (data: any) => {
-  // 属性
-  if (data.type === IoTThingModelTypeEnum.PROPERTY) {
-    removeDataSpecs(data.property)
-    data.dataType = data.property.dataType
-    data.property.identifier = data.identifier
-    data.property.name = data.name
-    delete data.service
-    delete data.event
-  }
-  // 服务
-  if (data.type === IoTThingModelTypeEnum.SERVICE) {
-    removeDataSpecs(data.service)
-    data.dataType = data.service.dataType
-    data.service.identifier = data.identifier
-    data.service.name = data.name
-    delete data.property
-    delete data.event
-  }
-  // 事件
-  if (data.type === IoTThingModelTypeEnum.EVENT) {
-    removeDataSpecs(data.event)
-    data.dataType = data.event.dataType
-    data.event.identifier = data.identifier
-    data.event.name = data.name
-    delete data.property
-    delete data.service
-  }
-}
-
-/** 处理 dataSpecs 为空的情况 */
-const removeDataSpecs = (val: any) => {
-  if (isEmpty(val.dataSpecs)) {
-    delete val.dataSpecs
-  }
-  if (isEmpty(val.dataSpecsList)) {
-    delete val.dataSpecsList
-  }
-}
-
-/** 重置表单 */
-const resetForm = () => {
-  formData.value = {
-    type: IoTThingModelTypeEnum.PROPERTY,
-    dataType: IoTDataSpecsDataTypeEnum.INT,
-    property: {
-      dataType: IoTDataSpecsDataTypeEnum.INT,
-      dataSpecs: {
-        dataType: IoTDataSpecsDataTypeEnum.INT
-      }
-    },
-    service: {},
-    event: {}
-  }
-  formRef.value?.resetFields()
-}
-</script>

+ 6 - 4
src/views/pms/constructionDailyReport/components/report-config.ts

@@ -42,6 +42,8 @@ export interface ConstructionDailyReportQuery {
   pageNo: number
   pageSize: number
   deptId?: number
+  projectDeptId?: number
+  teamId?: number
   projectName?: string
   wellName?: string
   createTime?: string[]
@@ -70,8 +72,8 @@ export const tableColumns: Array<{
   fixed?: 'left' | 'right'
 }> = [
   { prop: 'reportName', label: '日报名称', minWidth: 180, fixed: 'left' },
-  { prop: 'createTime', label: '创建(施工)日期', width: 120 },
-  { prop: 'constructionEndDate', label: '施工结束日期', width: 120 },
+  { prop: 'createTime', label: '创建日期', width: 120 },
+  // { prop: 'constructionEndDate', label: '施工结束日期', width: 120 },
   { prop: 'projectName', label: '项目', minWidth: 160, fixed: 'left' },
   { prop: 'location', label: '施工区域', minWidth: 140 },
   { prop: 'deptName', label: '队伍', minWidth: 140 },
@@ -81,7 +83,7 @@ export const tableColumns: Array<{
   { prop: 'fiveStatusLabel', label: '队伍状态', minWidth: 120 },
   { prop: 'dailyWorkingLayers', label: '当日主压层数', width: 120 },
   { prop: 'dailySandVolume', label: '当日加砂量(吨)', width: 130 },
-  { prop: 'dailyLiquidVolume', label: '当日注液量(m3)', width: 130 },
+  { prop: 'dailyLiquidVolume', label: '当日注液量()', width: 130 },
   { prop: 'monthWorkingWells', label: '当月作业井数', width: 120 },
   { prop: 'monthWorkingLayers', label: '当月作业层数', width: 120 },
   { prop: 'dailyFuel', label: '当日油耗(L)', width: 110 },
@@ -135,7 +137,7 @@ export const formSections: ConstructionDailyReportSection[] = [
       },
       {
         prop: 'dailyLiquidVolume',
-        label: '当日注液量()',
+        label: '当日注液量()',
         type: 'number',
         editableInEdit: true
       },

+ 53 - 6
src/views/pms/constructionDailyReport/index.vue

@@ -14,20 +14,58 @@ defineOptions({ name: 'ConstructionDailyReport' })
 
 const userStore = useUserStore()
 const deptId = userStore.getUser.deptId
+const route = useRoute()
+
+const parseQueryNumber = (value: unknown) => {
+  if (Array.isArray(value)) {
+    return parseQueryNumber(value[0])
+  }
+  if (value === undefined || value === null || value === '') {
+    return undefined
+  }
+  const parsedValue = Number(value)
+  return Number.isNaN(parsedValue) ? undefined : parsedValue
+}
 
-const initQuery: ConstructionDailyReportQuery = {
+const resolveCreateTimeFromRoute = () => {
+  const createTime = route.query.createTime
+  if (Array.isArray(createTime)) {
+    const values = createTime.filter((item): item is string => typeof item === 'string')
+    return values.length === 2 ? values : undefined
+  }
+  if (typeof createTime === 'string') {
+    const values = createTime.split(',').filter(Boolean)
+    return values.length === 2 ? values : undefined
+  }
+  return undefined
+}
+
+const createDefaultQuery = (): ConstructionDailyReportQuery => ({
   pageNo: 1,
   pageSize: 10,
-  deptId
-}
+  deptId: parseQueryNumber(route.query.deptId) ?? deptId,
+  projectDeptId: parseQueryNumber(route.query.projectDeptId),
+  teamId: parseQueryNumber(route.query.teamId),
+  createTime: resolveCreateTimeFromRoute()
+})
 
-const query = ref<ConstructionDailyReportQuery>({ ...initQuery })
+const query = ref<ConstructionDailyReportQuery>(createDefaultQuery())
 const list = ref<ConstructionDailyReportRow[]>([])
 const total = ref(0)
 const loading = ref(false)
 const visible = ref(false)
 const formRef = ref<InstanceType<typeof ConstructionDailyReportDrawer>>()
-const route = useRoute()
+
+function syncQueryFromRoute() {
+  const nextQuery = createDefaultQuery()
+  query.value = {
+    ...query.value,
+    deptId: nextQuery.deptId,
+    projectDeptId: nextQuery.projectDeptId,
+    teamId: nextQuery.teamId,
+    createTime: nextQuery.createTime
+  }
+}
 
 const loadList = useDebounceFn(async () => {
   loading.value = true
@@ -48,7 +86,7 @@ function handleQuery(setPage = true) {
 }
 
 function resetQuery() {
-  query.value = { ...initQuery }
+  query.value = createDefaultQuery()
   handleQuery()
 }
 
@@ -69,6 +107,8 @@ function handleOpenForm(id: number, type: 'edit' | 'readonly') {
 watch(
   [
     () => query.value.deptId,
+    () => query.value.projectDeptId,
+    () => query.value.teamId,
     () => query.value.projectName,
     () => query.value.wellName,
     () => query.value.createTime
@@ -77,6 +117,13 @@ watch(
   { immediate: true }
 )
 
+watch(
+  () => [route.query.deptId, route.query.projectDeptId, route.query.teamId, route.query.createTime],
+  () => {
+    syncQueryFromRoute()
+  }
+)
+
 onMounted(() => {
   if (route.query.id) {
     handleOpenForm(Number(route.query.id), 'edit')

+ 493 - 0
src/views/pms/fiveDailyReportSummary/index.vue

@@ -0,0 +1,493 @@
+<script setup>
+import dayjs from 'dayjs'
+import CountTo from '@/components/count-to1.vue'
+import { FiveIotProjectDailyReportApi } from '@/api/pms/fiveconstructiondailyreport'
+import { rangeShortcuts } from '@/utils/formatTime'
+import { useDebounceFn } from '@vueuse/core'
+import { useTableComponents } from '@/components/ZmTable/useTableComponents'
+
+defineOptions({ name: 'FiveDailyReportSummary' })
+
+const route = useRoute()
+const router = useRouter()
+const defaultDeptId = 388
+const { ZmTable, ZmTableColumn } = useTableComponents()
+
+const summaryCards = [
+  {
+    key: 'cumulativeWorkingLayers',
+    label: '压裂',
+    unit: '层',
+    icon: 'i-material-symbols-layers-outline-rounded',
+    decimals: 0
+  },
+  {
+    key: 'totalDailyFuel',
+    label: '油耗',
+    unit: 'L',
+    icon: 'i-material-symbols-local-gas-station-outline-rounded',
+    decimals: 0
+  },
+  {
+    key: 'cumulativeMixSand',
+    label: '加砂',
+    unit: '方',
+    icon: 'i-material-symbols-auto-mode-rounded',
+    decimals: 0
+  },
+  {
+    key: 'cumulativeLiquidVolume',
+    label: '注液',
+    unit: '方',
+    icon: 'i-material-symbols-water-drop-outline-rounded',
+    decimals: 0
+  },
+  {
+    key: 'utilizationRate',
+    label: '设备利用率',
+    unit: '%',
+    icon: 'i-material-symbols-build-outline-rounded',
+    decimals: 2
+  }
+]
+
+const tableColumns = [
+  { prop: 'projectDeptName', label: '部门', minWidth: 180 },
+  { prop: 'cumulativeLiquidVolume', label: '注液量(方)', minWidth: 140 },
+  { prop: 'cumulativeMixSand', label: '加砂(方)', minWidth: 140 },
+  { prop: 'cumulativeWorkingLayers', label: '压裂(层)', minWidth: 140 },
+  { prop: 'totalDailyFuel', label: '油耗(L)', minWidth: 140 },
+  { prop: 'utilizationRate', label: '设备利用率(%)', minWidth: 150 }
+]
+
+const createDefaultQuery = () => ({
+  pageNo: 1,
+  pageSize: 10,
+  deptId: defaultDeptId,
+  createTime: [
+    ...rangeShortcuts[1].value().map((item) => dayjs(item).format('YYYY-MM-DD HH:mm:ss'))
+  ]
+})
+
+const query = ref(createDefaultQuery())
+const list = ref([])
+const total = ref(0)
+const loading = ref(false)
+const summaryLoading = ref(false)
+const currentDeptName = ref('5号国')
+const selectedDeptType = ref('1')
+const cardStatistics = ref({
+  cumulativeWorkingLayers: 0,
+  totalDailyFuel: 0,
+  cumulativeMixSand: 0,
+  cumulativeLiquidVolume: 0,
+  utilizationRate: 0
+})
+
+const deptColumnLabel = computed(() => (selectedDeptType.value === '1' ? '项目部' : '队伍'))
+
+const syncQueryFromRoute = () => {
+  const createTime = route.query.createTime
+
+  if (Array.isArray(createTime)) {
+    const values = createTime.filter((item) => typeof item === 'string')
+    if (values.length === 2) {
+      query.value.createTime = values
+    }
+  } else if (typeof createTime === 'string') {
+    const values = createTime.split(',').filter(Boolean)
+    if (values.length === 2) {
+      query.value.createTime = values
+    }
+  }
+}
+
+const loadList = useDebounceFn(async () => {
+  loading.value = true
+  try {
+    const data = await FiveIotProjectDailyReportApi.getProjectDailyReportSummaryPage({
+      pageNo: query.value.pageNo,
+      pageSize: query.value.pageSize,
+      deptId: query.value.deptId,
+      createTime: query.value.createTime
+    })
+    list.value = (data?.list ?? []).map((item) => ({
+      ...item,
+      deptDisplayName:
+        String(item.type || selectedDeptType.value || '1') === '2'
+          ? item.projectDeptName || '-'
+          : item.teamName || item.projectDeptName || '-'
+    }))
+    total.value = data?.total ?? 0
+  } finally {
+    loading.value = false
+  }
+}, 200)
+
+const loadSummary = useDebounceFn(async () => {
+  summaryLoading.value = true
+  try {
+    const data = await FiveIotProjectDailyReportApi.getIotProjectDailyReportSummary({
+      deptId: query.value.deptId,
+      createTime: query.value.createTime
+    })
+
+    cardStatistics.value = {
+      cumulativeWorkingLayers: data?.cumulativeWorkingLayers || 0,
+      totalDailyFuel: data?.totalDailyFuel || 0,
+      cumulativeMixSand: data?.cumulativeMixSand || 0,
+      cumulativeLiquidVolume: data?.cumulativeLiquidVolume || 0,
+      utilizationRate: Number(data?.utilizationRate || 0)
+    }
+  } finally {
+    summaryLoading.value = false
+  }
+}, 200)
+
+function handleQuery(resetPage = true) {
+  if (resetPage) {
+    query.value.pageNo = 1
+  }
+  loadList()
+}
+
+function resetQuery() {
+  query.value = createDefaultQuery()
+  currentDeptName.value = '5号国'
+  loadSummary()
+  handleQuery()
+}
+
+function handleDeptNodeClick(node) {
+  currentDeptName.value = node?.name || '5号国'
+  selectedDeptType.value = String(node?.type || '1')
+  loadSummary()
+  handleQuery()
+}
+
+function handleSizeChange(value) {
+  query.value.pageSize = value
+  handleQuery(false)
+}
+
+function handleCurrentChange(value) {
+  query.value.pageNo = value
+  loadList()
+}
+
+function formatCellValue(row, prop) {
+  if (prop === 'utilizationRate') {
+    return row[prop] === null || row[prop] === undefined ? '-' : `${Number(row[prop]).toFixed(2)}%`
+  }
+
+  const value = row[prop]
+  return value === null || value === undefined || value === '' ? '-' : value
+}
+
+function handleDeptCellClick(row) {
+  const rowType = String(row?.type || selectedDeptType.value || '1')
+  const targetQuery = {
+    deptId: String(query.value.deptId || defaultDeptId),
+    createTime: Array.isArray(query.value.createTime) ? query.value.createTime : undefined,
+    projectDeptId: undefined,
+    teamId: undefined
+  }
+
+  if (rowType === '2' && row?.projectDeptId) {
+    targetQuery.projectDeptId = String(row.projectDeptId)
+  } else if (row?.teamId) {
+    targetQuery.teamId = String(row.teamId)
+  } else if (row?.projectDeptId) {
+    targetQuery.projectDeptId = String(row.projectDeptId)
+  } else {
+    return
+  }
+
+  router.push({
+    name: 'ConstructionDailyReport',
+    query: targetQuery
+  })
+}
+
+syncQueryFromRoute()
+
+watch(
+  () => [query.value.createTime?.[0], query.value.createTime?.[1]],
+  () => {
+    loadSummary()
+    handleQuery()
+  },
+  { immediate: true }
+)
+
+watch(
+  () => query.value.deptId,
+  () => {
+    loadSummary()
+    handleQuery()
+  },
+  { immediate: true }
+)
+
+watch(
+  () => route.query.createTime,
+  () => {
+    syncQueryFromRoute()
+  }
+)
+</script>
+
+<template>
+  <div
+    class="grid grid-cols-[auto_1fr] grid-rows-[48px_44px_1fr] gap-3 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]">
+    <DeptTreeSelect
+      v-model="query.deptId"
+      :top-id="388"
+      :deptId="defaultDeptId"
+      :show-title="false"
+      class="row-span-3"
+      @node-click="handleDeptNodeClick" />
+
+    <el-form
+      size="small"
+      label-width="auto"
+      class="bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-8 pt-3 pb-4 flex items-center flex-wrap min-w-0">
+      <div class="flex items-center gap-2 flex-wrap">
+        <el-form-item label="创建时间">
+          <el-date-picker
+            v-model="query.createTime"
+            value-format="YYYY-MM-DD HH:mm:ss"
+            type="daterange"
+            start-placeholder="开始日期"
+            end-placeholder="结束日期"
+            :shortcuts="rangeShortcuts"
+            :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+            class="!w-260px" />
+        </el-form-item>
+
+        <el-form-item>
+          <el-button type="primary" @click="handleQuery()">
+            <Icon icon="ep:search" class="mr-5px" /> 搜索
+          </el-button>
+          <el-button @click="resetQuery">
+            <Icon icon="ep:refresh" class="mr-5px" /> 重置
+          </el-button>
+        </el-form-item>
+      </div>
+    </el-form>
+
+    <div class="summary-cards min-w-0" v-loading="summaryLoading">
+      <article
+        v-for="card in summaryCards"
+        :key="card.key"
+        class="summary-card bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-5 py-8 min-w-0">
+        <div class="summary-card__icon" :class="card.icon"></div>
+        <div class="min-w-0">
+          <div class="summary-card__label">{{ card.label }}({{ card.unit }})</div>
+          <count-to
+            class="summary-card__value"
+            :start-val="0"
+            :end-val="cardStatistics[card.key]"
+            :decimals="card.decimals || 0">
+            <span class="summary-card__placeholder">--</span>
+          </count-to>
+        </div>
+      </article>
+    </div>
+
+    <div class="summary-content min-h-0 min-w-0 overflow-hidden">
+      <div class="summary-content__header">
+        <h3 class="summary-content__title">{{ currentDeptName }}日报汇总</h3>
+      </div>
+
+      <div class="summary-table-wrap">
+        <el-auto-resizer class="absolute">
+          <template #default="{ width, height }">
+            <ZmTable
+              :data="list"
+              :loading="loading"
+              :width="width"
+              :height="height"
+              :max-height="height"
+              show-border>
+              <ZmTableColumn type="index" label="序号" :width="70" fixed="left" />
+              <ZmTableColumn
+                prop="deptDisplayName"
+                :label="deptColumnLabel"
+                :min-width="180"
+                show-overflow-tooltip
+                cover-formatter
+                :real-value="(row) => formatCellValue(row, 'deptDisplayName')">
+                <template #default="{ row }">
+                  <el-button
+                    link
+                    type="primary"
+                    class="summary-table-link"
+                    @click.stop="handleDeptCellClick(row)">
+                    {{ formatCellValue(row, 'deptDisplayName') }}
+                  </el-button>
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn
+                v-for="column in tableColumns.slice(1)"
+                :key="column.prop"
+                :prop="column.prop"
+                :label="column.label"
+                :min-width="column.minWidth"
+                show-overflow-tooltip
+                cover-formatter
+                :real-value="(row) => formatCellValue(row, column.prop)" />
+            </ZmTable>
+          </template>
+        </el-auto-resizer>
+      </div>
+
+      <div class="h-8 mt-2 flex items-center justify-end">
+        <el-pagination
+          v-show="total > 0"
+          :current-page="query.pageNo"
+          :page-size="query.pageSize"
+          :background="true"
+          :page-sizes="[10, 20, 30, 50, 100]"
+          :total="total"
+          layout="total, sizes, prev, pager, next, jumper"
+          @size-change="handleSizeChange"
+          @current-change="handleCurrentChange" />
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+:deep(.el-form-item) {
+  margin-bottom: 0;
+}
+
+.summary-cards {
+  min-width: 0;
+  display: grid;
+  grid-template-columns: repeat(5, minmax(0, 1fr));
+  gap: 16px;
+}
+
+.summary-card {
+  position: relative;
+  display: flex;
+  align-items: center;
+  gap: 14px;
+  overflow: hidden;
+}
+
+/* .summary-card::after {
+  content: '';
+  position: absolute;
+  inset: auto -32px -32px auto;
+  width: 66px;
+  height: 66px;
+  border-radius: 999px;
+  background: linear-gradient(135deg, rgb(59 130 246 / 0.14), rgb(16 185 129 / 0.08));
+} */
+
+.summary-card__icon {
+  position: relative;
+  z-index: 1;
+  flex: none;
+  width: 44px;
+  height: 44px;
+  border-radius: 14px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 24px;
+  color: #2563eb;
+  background: linear-gradient(135deg, rgb(37 99 235 / 0.14), rgb(14 165 233 / 0.08));
+}
+
+.summary-card__label {
+  position: relative;
+  z-index: 1;
+  color: var(--el-text-color-regular);
+  font-size: 13px;
+  line-height: 20px;
+  margin-bottom: 4px;
+}
+
+.summary-card__value {
+  position: relative;
+  z-index: 1;
+  font-size: 28px;
+  font-weight: 600;
+  line-height: 1;
+  color: var(--el-text-color-primary);
+}
+
+.summary-card__placeholder {
+  font-size: 16px;
+  color: var(--el-text-color-placeholder);
+}
+
+.summary-content {
+  background: #fff;
+  border-radius: 12px;
+  box-shadow: var(--el-box-shadow-light);
+  padding: 16px;
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+  margin-top: 20px;
+}
+
+.summary-content__header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px;
+}
+
+.summary-content__title {
+  font-size: 18px;
+  line-height: 28px;
+  font-weight: 600;
+  color: var(--el-text-color-primary);
+}
+
+.summary-table-wrap {
+  position: relative;
+  flex: 1;
+  min-height: 0;
+}
+
+.summary-table-link {
+  padding: 0;
+  font-weight: 500;
+}
+
+@media (max-width: 1600px) {
+  .summary-card__value {
+    font-size: 24px;
+  }
+}
+
+@media (max-width: 1280px) {
+  .summary-card {
+    gap: 10px;
+    padding: 12px 14px;
+  }
+
+  .summary-card__icon {
+    width: 38px;
+    height: 38px;
+    border-radius: 12px;
+    font-size: 20px;
+  }
+
+  .summary-card__label {
+    font-size: 12px;
+    line-height: 18px;
+  }
+
+  .summary-card__value {
+    font-size: 20px;
+  }
+}
+</style>

+ 478 - 7
src/views/pms/qhse/kanban/index.vue

@@ -20,6 +20,7 @@ import {
 import {
   kanbanApi,
   IotDangerApi,
+  IotHiddenApi,
   IotInstrumentApi,
   IotMeasureCertApi,
   IotCertificateApi
@@ -207,8 +208,8 @@ const permitStats = ref<PermitStat[]>([
 ])
 
 const qualificationWarnings = ref([
-  { label: '证件过期', value: 0, accent: '#ff7a7a' },
-  { label: '即将到期', value: 0, accent: '#e6ab00' }
+  { label: '证件过期', value: 0, accent: '#ff7a7a', key: 'expired' },
+  { label: '即将到期', value: 0, accent: '#e6ab00', key: 'warn' }
 ])
 
 const qhseMetricTabs = [
@@ -223,6 +224,7 @@ const qhseMetricTabs = [
 ] as const
 
 const activeQhseMetric = ref<QhseMetricValue>('ltir')
+const dangerGradeOptions = computed(() => getStrDictOptions(DICT_TYPE.DANGER_GRADE))
 
 const qhseTrendSeries = ref<Record<QhseMetricValue, Array<{ year: string; value: number }>>>({
   ltir: [
@@ -355,12 +357,19 @@ const safeDayBarData = ref<SafeDayBarItem[]>([])
 const summaryPanel = ref<any>(null)
 const total = ref(0)
 const instrumentExpired = ref(0)
+const safeDayDisplayOrder = ['瑞气能源', '瑞霖能源', '5#项目', '酸化压裂', '气体增产', '钻修井']
 
 function buildSafeDayBarData(baseData: SafeDayMap) {
-  safeDayBarData.value = Object.entries(baseData || {}).map(([name, value]) => ({
+  const mappedData = Object.entries(baseData || {}).map(([name, value]) => ({
     name,
     value: Number(value) || 0
   }))
+  const orderedData = safeDayDisplayOrder
+    .map((name) => mappedData.find((item) => item.name === name))
+    .filter(Boolean) as SafeDayBarItem[]
+  const remainingData = mappedData.filter((item) => !safeDayDisplayOrder.includes(item.name))
+
+  safeDayBarData.value = [...orderedData, ...remainingData]
 }
 
 async function getStatic() {
@@ -503,6 +512,8 @@ function initHazardChart() {
     renderer: CHART_RENDERER
   })
   hazardChart.setOption(getHazardChartOption(), true)
+  hazardChart.off('click')
+  hazardChart.on('click', handleHazardBarClick)
 }
 
 function updateHazardChart() {
@@ -574,6 +585,7 @@ function getSafeDayChartOption(): echarts.EChartsOption {
     },
     yAxis: {
       type: 'category',
+      inverse: true,
       data: safeDayBarData.value.map((item) => item.name),
       axisLine: { show: false },
       axisTick: { show: false },
@@ -946,6 +958,57 @@ const certQueryParams = reactive({
   alertWarn: undefined as string | undefined
 })
 
+const certWarningDialogVisible = ref(false)
+const certWarningDialogTitle = ref('')
+const certWarningLoading = ref(false)
+const certWarningList = ref<any[]>([])
+const certWarningTotal = ref(0)
+const certWarningQueryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  deptId: userStore.getUser.deptId,
+  type: undefined as string | undefined,
+  classify: undefined as string | undefined,
+  userName: undefined as string | undefined,
+  expired: undefined as boolean | undefined,
+  alertWarn: undefined as boolean | undefined
+})
+
+const hazardDialogVisible = ref(false)
+const hazardDialogTitle = ref('')
+const hazardDialogLoading = ref(false)
+const hazardDialogList = ref<any[]>([])
+const hazardDialogTotal = ref(0)
+const hazardDialogQueryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  deptId: userStore.getUser.deptId,
+  status: undefined as string | undefined,
+  address: '',
+  source: ''
+})
+
+const dangerDialogVisible = ref(false)
+const dangerDialogTitle = ref('')
+const dangerDialogLoading = ref(false)
+const dangerDialogList = ref<any[]>([])
+const dangerDialogTotal = ref(0)
+const dangerDialogQueryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  riskGrade: '',
+  deptId: userStore.getUser.deptId
+})
+
+const getCertificateTypeText = (type: string) => {
+  const map: Record<string, string> = {
+    personal: '个人证书',
+    organization: '组织证书',
+    other: '其他'
+  }
+  return map[type] || type
+}
+
 function formatDateCorrectly(timestamp: any) {
   if (!timestamp) return ''
   let time = Number(timestamp)
@@ -1016,6 +1079,103 @@ async function getCertificateBoardList() {
   }
 }
 
+async function getCertificateWarningList() {
+  certWarningLoading.value = true
+  try {
+    const data = await IotMeasureCertApi.getIotMeasureCertPage(certWarningQueryParams)
+    certWarningList.value = data.list || []
+    certWarningTotal.value = data.total || 0
+  } finally {
+    certWarningLoading.value = false
+  }
+}
+
+async function getHazardDialogList() {
+  hazardDialogLoading.value = true
+  try {
+    const data = await IotHiddenApi.getHiddenList(hazardDialogQueryParams)
+    hazardDialogList.value = data.list || []
+    hazardDialogTotal.value = data.total || 0
+  } finally {
+    hazardDialogLoading.value = false
+  }
+}
+
+async function getDangerDialogList() {
+  dangerDialogLoading.value = true
+  try {
+    const data = await IotDangerApi.getDangerList(dangerDialogQueryParams)
+    dangerDialogList.value = data.list || []
+    dangerDialogTotal.value = data.total || 0
+  } finally {
+    dangerDialogLoading.value = false
+  }
+}
+
+function openHazardDialog(barLabel: string) {
+  hazardDialogQueryParams.pageNo = 1
+  hazardDialogQueryParams.pageSize = 10
+  hazardDialogQueryParams.deptId = userStore.getUser.deptId
+  hazardDialogQueryParams.address = ''
+  hazardDialogQueryParams.source = ''
+
+  if (barLabel === '未整改') {
+    hazardDialogTitle.value = '未整改隐患列表'
+    hazardDialogQueryParams.status = 'todo'
+  } else if (barLabel === '已整改') {
+    hazardDialogTitle.value = '已整改隐患列表'
+    hazardDialogQueryParams.status = 'finished'
+  } else {
+    hazardDialogTitle.value = '隐患总数列表'
+    hazardDialogQueryParams.status = undefined
+  }
+
+  hazardDialogVisible.value = true
+  getHazardDialogList()
+}
+
+function openDangerDialog(zone: { title?: string }) {
+  const riskGradeLabel = String(zone.title || '')
+    .replace(/区$/, '')
+    .trim()
+  const matchedOption = dangerGradeOptions.value.find((item) => item.label === riskGradeLabel)
+  const riskGrade = matchedOption?.value || ''
+  dangerDialogQueryParams.pageNo = 1
+  dangerDialogQueryParams.pageSize = 10
+  dangerDialogQueryParams.deptId = userStore.getUser.deptId
+  dangerDialogQueryParams.riskGrade = riskGrade
+  dangerDialogTitle.value = riskGradeLabel ? `${riskGradeLabel}风险列表` : '风险列表'
+  dangerDialogVisible.value = true
+  getDangerDialogList()
+}
+
+function handleHazardBarClick(params: { name?: string }) {
+  if (!params?.name) return
+  openHazardDialog(params.name)
+}
+
+function openCertificateWarningDialog(type: 'expired' | 'warn') {
+  certWarningQueryParams.pageNo = 1
+  certWarningQueryParams.pageSize = 10
+  certWarningQueryParams.deptId = userStore.getUser.deptId
+  certWarningQueryParams.type = undefined
+  certWarningQueryParams.classify = undefined
+  certWarningQueryParams.userName = undefined
+
+  if (type === 'expired') {
+    certWarningDialogTitle.value = '证件过期'
+    certWarningQueryParams.expired = true
+    certWarningQueryParams.alertWarn = undefined
+  } else {
+    certWarningDialogTitle.value = '证件即将到期'
+    certWarningQueryParams.expired = undefined
+    certWarningQueryParams.alertWarn = true
+  }
+
+  certWarningDialogVisible.value = true
+  getCertificateWarningList()
+}
+
 function handleCertificateQuery() {
   certQueryParams.pageNo = 1
   getCertificateBoardList()
@@ -1182,7 +1342,11 @@ onUnmounted(() => {
                   风险管控及隐患治理
                 </div>
                 <div class="risk-grid">
-                  <article v-for="zone in riskZones" :key="zone.title" class="risk-card">
+                  <article
+                    v-for="zone in riskZones"
+                    :key="zone.title"
+                    class="risk-card risk-card--clickable"
+                    @click="openDangerDialog(zone)">
                     <div class="risk-card__title">
                       <span class="risk-card__dot" :style="{ background: zone.color }"></span>
                       <span :style="{ color: zone.color }">{{ zone.title }}</span>
@@ -1262,7 +1426,8 @@ onUnmounted(() => {
                       <div
                         v-for="item in qualificationWarnings"
                         :key="item.label"
-                        class="qualification-item">
+                        class="qualification-item qualification-item--clickable"
+                        @click="openCertificateWarningDialog(item.key)">
                         <span class="qualification-item__label">{{ item.label }}:</span>
                         <strong :style="{ color: item.accent }">
                           <CountTo
@@ -1401,6 +1566,17 @@ onUnmounted(() => {
               </el-table-column>
 
               <el-table-column label="备注" align="center" prop="remark" />
+              <el-table-column label="操作" align="center" fixed="right" min-width="180px" action>
+                <template #default="scope">
+                  <el-button
+                    link
+                    type="success"
+                    v-if="scope.row.certPic"
+                    @click="viewFile(scope.row.certPic)">
+                    查看证书
+                  </el-button>
+                </template>
+              </el-table-column>
             </el-table>
 
             <div class="certificate-pagination">
@@ -1453,13 +1629,279 @@ onUnmounted(() => {
       </div>
     </template>
   </Dialog>
+
+  <Dialog v-model="certWarningDialogVisible" :title="certWarningDialogTitle" width="1100px">
+    <div class="certificate-warning-dialog">
+      <el-table
+        :loading="certWarningLoading"
+        :data="certWarningList"
+        :row-style="tableRowStyle"
+        :row-class-name="tableRowClassName"
+        height="400"
+        class="device-list-table production-brief-table">
+        <el-table-column label="序号" width="80" align="center">
+          <template #default="scope">
+            {{
+              (certWarningQueryParams.pageNo - 1) * certWarningQueryParams.pageSize +
+              scope.$index +
+              1
+            }}
+          </template>
+        </el-table-column>
+        <el-table-column label="证书类型" align="center" prop="type" width="120">
+          <template #default="scope">
+            {{ getCertificateTypeText(scope.row.type) }}
+          </template>
+        </el-table-column>
+        <el-table-column label="证书类别" align="center" width="150" prop="classify">
+          <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="证书名称"
+          align="center"
+          prop="certName"
+          show-overflow-tooltip
+          width="100" />
+        <el-table-column label="所属人" align="center" prop="userName" width="100" />
+        <el-table-column
+          label="所在部门"
+          align="center"
+          prop="deptName"
+          width="100"
+          show-overflow-tooltip />
+        <el-table-column
+          label="颁发机构"
+          align="center"
+          prop="certOrg"
+          width="180"
+          show-overflow-tooltip />
+        <el-table-column
+          label="证书标准"
+          align="center"
+          prop="certStandard"
+          width="100"
+          show-overflow-tooltip />
+        <el-table-column label="颁发时间" align="center" prop="certIssue" width="120">
+          <template #default="scope">
+            {{ formatDateCorrectly(scope.row.certIssue) }}
+          </template>
+        </el-table-column>
+        <el-table-column label="有效期" align="center" prop="certExpire" width="120">
+          <template #default="scope">
+            {{ formatDateCorrectly(scope.row.certExpire) }}
+          </template>
+        </el-table-column>
+        <el-table-column label="备注" align="center" prop="remark" show-overflow-tooltip />
+        <el-table-column label="操作" align="center" fixed="right" min-width="100px">
+          <template #default="scope">
+            <el-button
+              link
+              type="success"
+              v-if="scope.row.certPic"
+              @click="viewFile(scope.row.certPic)">
+              查看证书
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <div class="certificate-pagination">
+        <Pagination
+          :total="certWarningTotal"
+          v-model:page="certWarningQueryParams.pageNo"
+          v-model:limit="certWarningQueryParams.pageSize"
+          @pagination="getCertificateWarningList" />
+      </div>
+    </div>
+  </Dialog>
+
+  <Dialog v-model="hazardDialogVisible" :title="hazardDialogTitle" width="1400px">
+    <div class="certificate-warning-dialog">
+      <el-table
+        :loading="hazardDialogLoading"
+        :data="hazardDialogList"
+        height="400"
+        class="device-list-table production-brief-table">
+        <el-table-column label="序号" width="80" align="center">
+          <template #default="scope">
+            {{
+              (hazardDialogQueryParams.pageNo - 1) * hazardDialogQueryParams.pageSize +
+              scope.$index +
+              1
+            }}
+          </template>
+        </el-table-column>
+        <el-table-column label="单位" align="center" prop="deptName" min-width="140" />
+        <el-table-column label="类型" align="center" prop="classifyName" min-width="120" />
+        <el-table-column label="来源" align="center" min-width="110">
+          <template #default="scope">
+            <dict-tag :type="DICT_TYPE.QHSE_HAZARD_SOURCE" :value="scope.row.source" />
+          </template>
+        </el-table-column>
+        <el-table-column label="地点" align="center" prop="address" min-width="140" />
+        <el-table-column
+          label="存在问题"
+          align="center"
+          prop="problem"
+          min-width="180"
+          show-overflow-tooltip />
+        <el-table-column label="隐患附件" align="center" min-width="100">
+          <template #default="{ row }">
+            <el-button v-if="row.hazardFile" link type="primary" @click="viewFile(row.hazardFile)">
+              查看
+            </el-button>
+            <span v-else>-</span>
+          </template>
+        </el-table-column>
+        <el-table-column
+          label="整改人"
+          align="center"
+          prop="correctPersonName"
+          min-width="100"
+          show-overflow-tooltip />
+        <el-table-column
+          label="整改要求"
+          align="center"
+          prop="correctStandard"
+          min-width="160"
+          show-overflow-tooltip />
+        <el-table-column
+          label="整改情况"
+          align="center"
+          prop="rectifyDesc"
+          min-width="160"
+          show-overflow-tooltip />
+        <el-table-column label="整改附件" align="center" min-width="100">
+          <template #default="{ row }">
+            <el-button
+              v-if="row.rectifyFile"
+              link
+              type="primary"
+              @click="viewFile(row.rectifyFile)">
+              查看
+            </el-button>
+            <span v-else>-</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="排查时间" align="center" min-width="120">
+          <template #default="{ row }">
+            {{ formatDateCorrectly(row.checkTime) }}
+          </template>
+        </el-table-column>
+        <el-table-column label="状态" align="center" min-width="90">
+          <template #default="scope">
+            <dict-tag :type="DICT_TYPE.QHSE_HAZARD_STATUS" :value="scope.row.status" />
+          </template>
+        </el-table-column>
+        <el-table-column
+          label="备注"
+          align="center"
+          prop="remark"
+          min-width="120"
+          show-overflow-tooltip />
+      </el-table>
+
+      <div class="certificate-pagination">
+        <Pagination
+          :total="hazardDialogTotal"
+          v-model:page="hazardDialogQueryParams.pageNo"
+          v-model:limit="hazardDialogQueryParams.pageSize"
+          @pagination="getHazardDialogList" />
+      </div>
+    </div>
+  </Dialog>
+
+  <Dialog v-model="dangerDialogVisible" :title="dangerDialogTitle" width="1400px">
+    <div class="certificate-warning-dialog">
+      <el-table
+        :loading="dangerDialogLoading"
+        :data="dangerDialogList"
+        height="400"
+        class="device-list-table production-brief-table">
+        <el-table-column label="序号" width="80" align="center">
+          <template #default="scope">
+            {{
+              (dangerDialogQueryParams.pageNo - 1) * dangerDialogQueryParams.pageSize +
+              scope.$index +
+              1
+            }}
+          </template>
+        </el-table-column>
+        <el-table-column
+          label="区域/位置"
+          align="center"
+          prop="region"
+          min-width="140"
+          show-overflow-tooltip />
+        <el-table-column
+          label="危险因素描述"
+          align="center"
+          prop="elementDescription"
+          min-width="180"
+          show-overflow-tooltip />
+        <el-table-column
+          label="可导致的后果"
+          align="center"
+          prop="maybeResult"
+          min-width="180"
+          show-overflow-tooltip />
+        <el-table-column label="风险评价" align="center">
+          <el-table-column prop="evalKn" label="可能性 (L)" width="100" align="center">
+            <template #default="{ row }">
+              {{ row.evalKn }}
+            </template>
+          </el-table-column>
+          <el-table-column prop="evalYz" label="严重性 (S)" width="100" align="center">
+            <template #default="{ row }">
+              {{ row.evalYz }}
+            </template>
+          </el-table-column>
+          <el-table-column prop="evalFxz" label="风险值 (R)" width="100" align="center">
+            <template #default="{ row }">
+              {{ row.evalFxz }}
+            </template>
+          </el-table-column>
+          <el-table-column prop="riskGrade" label="风险等级" width="100" align="center">
+            <template #default="scope">
+              <dict-tag :type="DICT_TYPE.DANGER_GRADE" :value="scope.row.riskGrade" />
+            </template>
+          </el-table-column>
+        </el-table-column>
+        <el-table-column
+          label="控制措施"
+          align="center"
+          prop="controlMethod"
+          show-overflow-tooltip />
+        <el-table-column
+          label="责任人"
+          align="center"
+          prop="charge"
+          min-width="100"
+          show-overflow-tooltip />
+      </el-table>
+
+      <div class="certificate-pagination">
+        <Pagination
+          :total="dangerDialogTotal"
+          v-model:page="dangerDialogQueryParams.pageNo"
+          v-model:limit="dangerDialogQueryParams.pageSize"
+          @pagination="getDangerDialogList" />
+      </div>
+    </div>
+  </Dialog>
 </template>
 
 <style lang="scss" scoped>
 @import url('@/styles/qhse_kb.scss');
 
 :deep(.el-select__wrapper) {
-  height: 38px !important;
+  // height: 38px !important;
   font-size: 14px !important;
 }
 .qhse-board {
@@ -1529,6 +1971,17 @@ onUnmounted(() => {
   color: #5c7393;
 }
 
+.risk-card--clickable {
+  cursor: pointer;
+  transition:
+    transform 0.2s ease,
+    box-shadow 0.2s ease;
+}
+
+.risk-card--clickable:hover {
+  transform: translateY(-2px);
+}
+
 .summary-toolbar__picker {
   width: 190px;
 }
@@ -1820,10 +2273,28 @@ onUnmounted(() => {
   color: #516785;
 }
 
+.qualification-item--clickable {
+  cursor: pointer;
+  transition:
+    transform 0.18s ease,
+    opacity 0.18s ease;
+}
+
+.qualification-item--clickable:hover {
+  transform: translateX(4px);
+  opacity: 0.9;
+}
+
 .qualification-item strong {
   font-size: 28px;
 }
 
+.certificate-warning-dialog {
+  display: flex;
+  flex-direction: column;
+  gap: 14px;
+}
+
 .bottom-grid {
   display: grid;
   margin-top: 10px;
@@ -1968,7 +2439,7 @@ onUnmounted(() => {
 }
 
 .certificate-pagination {
-  padding-top: 14px;
+  padding-top: 0px;
 }
 
 .production-brief-table {

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

@@ -530,6 +530,32 @@ const getList = async () => {
   }
 }
 
+const downloadFile = (response: any) => {
+  const blob = new Blob([response], {
+    type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8'
+  })
+
+  let fileName = '排查治理记录.xlsx'
+  const disposition = response.headers ? response.headers['content-disposition'] : ''
+  if (disposition) {
+    const filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/
+    const matches = filenameRegex.exec(disposition)
+    if (matches != null && matches[1]) {
+      fileName = matches[1].replace(/['"]/g, '')
+    }
+  }
+
+  const url = window.URL.createObjectURL(blob)
+  const link = document.createElement('a')
+  link.href = url
+  link.setAttribute('download', fileName)
+
+  document.body.appendChild(link)
+  link.click()
+
+  document.body.removeChild(link)
+  window.URL.revokeObjectURL(url)
+}
 const handleExport = async () => {
   try {
     exportLoading.value = true