Explorar o código

设备分类查询

yanghao hai 1 semana
pai
achega
f2a182f8e6

+ 1 - 1
.env.local

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

+ 14 - 0
src/views/pms/device/index.vue

@@ -809,6 +809,7 @@ const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
 const { push } = useRouter() // 路由跳转
 const deptList = ref<Tree[]>([]) // 树形结构
+const route = useRoute()
 const rootDeptId = 156
 const deptId = useUserStore().getUser.deptId || rootDeptId
 
@@ -940,6 +941,10 @@ const getDashboardData = async () => {
 }
 
 /** 处理部门被点击 */
+const syncRouteCodeQuery = () => {
+  queryParams.yfClass = route.query.code ? String(route.query.code) : undefined
+}
+
 const handleDeptNodeClick = async (row) => {
   queryParams.deptId = row.id
   await Promise.all([getList(), getDashboardData()])
@@ -1027,6 +1032,7 @@ const handleExport = async () => {
 
 /** 初始化 **/
 onMounted(async () => {
+  syncRouteCodeQuery()
   productClassifyList.value = handleTree(
     await ProductClassifyApi.IotProductClassifyApi.getSimpleProductClassifyList()
   )
@@ -1044,6 +1050,14 @@ onMounted(async () => {
 
   deptList.value = handleTree(await DeptApi.getSimpleDeptList())
 })
+
+watch(
+  () => route.query.code,
+  async () => {
+    syncRouteCodeQuery()
+    await Promise.all([getList(), getDashboardData()])
+  }
+)
 </script>
 <style scoped lang="scss">
 @media (width <= 1440px) {

+ 175 - 64
src/views/pms/qhse/kanban/index.vue

@@ -17,7 +17,13 @@ import {
   Search,
   Refresh
 } from '@element-plus/icons-vue'
-import { kanbanApi, IotDangerApi, IotInstrumentApi, IotMeasureCertApi } from '@/api/pms/qhse/index'
+import {
+  kanbanApi,
+  IotDangerApi,
+  IotInstrumentApi,
+  IotMeasureCertApi,
+  IotCertificateApi
+} from '@/api/pms/qhse/index'
 import { useUserStore } from '@/store/modules/user'
 import {
   ANIMATION,
@@ -871,6 +877,26 @@ function destroySocChart() {
   socChart = null
 }
 
+const tableRowStyle = ({ row }) => {
+  if (row.expired) {
+    return { backgroundColor: '#ffe6e6' }
+  }
+  if (row.alertWarn) {
+    return { backgroundColor: '#e19f1a' }
+  }
+  return {}
+}
+
+const tableRowClassName = ({ row }) => {
+  if (row.expired) {
+    return 'expired-row'
+  }
+  if (row.alertWarn) {
+    return 'alert-warn-row'
+  }
+  return ''
+}
+
 function formatPercent(numerator: number, denominator: number) {
   if (!denominator || Number.isNaN(denominator)) {
     return '0%'
@@ -915,9 +941,7 @@ 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,
   alertWarn: undefined as string | undefined
 })
@@ -931,22 +955,59 @@ function formatDateCorrectly(timestamp: any) {
   return formatDate(time).substring(0, 10)
 }
 
-function getCertificateTypeText(type: string) {
-  const map: Record<string, string> = {
-    personal: '个人证书',
-    organization: '组织证书',
-    other: '其他'
+let dialogFileView = ref(false)
+let fileList = ref([])
+const viewFile = (file) => {
+  fileList.value = file.split(',')
+  dialogFileView.value = true
+  // window.open(file)
+}
+
+const viewFileInfo = (file) => {
+  window.open(
+    'http://doc.deepoil.cc:8012/onlinePreview?url=' + encodeURIComponent(Base64.encode(file))
+  )
+}
+
+const extractFileName = (url: string): string => {
+  try {
+    // 移除查询参数和哈希
+    const cleanUrl = url.split('?')[0].split('#')[0]
+    // 获取最后一个斜杠后的内容
+    const parts = cleanUrl.split('/')
+    const fileName = parts[parts.length - 1]
+    // URL 解码
+    return decodeURIComponent(fileName) || url
+  } catch {
+    // 如果解析失败,返回原始 URL
+    return url
+  }
+}
+
+const handleDownload = async (url) => {
+  try {
+    const response = await fetch(url)
+    const blob = await response.blob()
+    const downloadUrl = window.URL.createObjectURL(blob)
+
+    const link = document.createElement('a')
+    link.href = downloadUrl
+    link.download = url.split('/').pop() // 自动获取文件名‌:ml-citation{ref="3" data="citationList"}
+    link.click()
+
+    URL.revokeObjectURL(downloadUrl)
+  } catch (error) {
+    console.error('下载失败:', error)
   }
-  return map[type] || type
 }
 
 async function getCertificateBoardList() {
   certLoading.value = true
   try {
-    const data = await IotMeasureCertApi.getIotMeasureCertPage({
-      ...certQueryParams,
-      deptId: userStore.user.deptId,
-      type: 'organization'
+    const data = await IotCertificateApi.getCertificateList({
+      ...certQueryParams
+      // deptId: userStore.user.deptId
+      // type: 'organization'
     })
     certTableData.value = data.list || []
     certTotal.value = data.total || 0
@@ -963,9 +1024,7 @@ function handleCertificateQuery() {
 function resetCertificateQuery() {
   certQueryParams.pageNo = 1
   certQueryParams.pageSize = 10
-  certQueryParams.type = undefined
-  certQueryParams.classify = undefined
-  certQueryParams.userName = ''
+
   certQueryParams.expired = undefined
   certQueryParams.alertWarn = undefined
   getCertificateBoardList()
@@ -1252,31 +1311,6 @@ onUnmounted(() => {
         <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.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
@@ -1320,6 +1354,8 @@ onUnmounted(() => {
             <el-table
               :loading="certLoading"
               :data="certTableData"
+              :row-style="tableRowStyle"
+              :row-class-name="tableRowClassName"
               height="100vh"
               :header-cell-style="{
                 height: '42px'
@@ -1331,41 +1367,40 @@ onUnmounted(() => {
                 </template>
               </el-table-column>
 
-              <el-table-column label="证书类型" align="center" min-width="120">
+              <el-table-column label="证书类别" align="center" width="150" prop="classify">
                 <template #default="scope">
-                  {{ getCertificateTypeText(scope.row.type) }}
+                  <dict-tag :type="DICT_TYPE.ORG_CERT" :value="scope.row.classify" />
                 </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"
+                label="证书名称"
                 align="center"
-                min-width="140" />
-              <el-table-column label="颁发时间" align="center" min-width="120">
+                prop="certName"
+                show-overflow-tooltip />
+
+              <el-table-column label="所在部门" align="center" prop="deptName" />
+
+              <el-table-column label="颁发机构" align="center" prop="certOrg" />
+
+              <el-table-column label="证书标准" align="center" prop="certStandard" />
+
+              <el-table-column label="颁发时间" align="center" prop="certIssue">
                 <template #default="scope">
                   {{ formatDateCorrectly(scope.row.certIssue) }}
                 </template>
               </el-table-column>
-              <el-table-column label="有效期" align="center" min-width="120">
+
+              <el-table-column label="有效期" align="center">
                 <template #default="scope">
                   {{ formatDateCorrectly(scope.row.certExpire) }}
                 </template>
               </el-table-column>
+
+              <el-table-column label="到期提醒" align="center">
+                <template #default="scope"> {{ scope.row.noticeBefore }}天前提醒 </template>
+              </el-table-column>
+
+              <el-table-column label="备注" align="center" prop="remark" />
             </el-table>
 
             <div class="certificate-pagination">
@@ -1375,11 +1410,49 @@ onUnmounted(() => {
                 v-model:limit="certQueryParams.pageSize"
                 @pagination="getCertificateBoardList" />
             </div>
+
+            <div class="shadow rounded-lg p-3 min-w-0 flex gap-4 justify-between">
+              <div
+                class="bg-[#fef0f0] text-[#f57979] px-3 flex-1 flex items-center justify-center text-center py-2 text-sm rounded-md"
+                ><el-icon class="stats-card__icon" :size="28">
+                  <Icon icon="ep:info-filled" /> </el-icon
+                >证书已过期红色预警</div
+              >
+              <div
+                class="bg-[#fdf6ec] flex-1 text-[#e6a23c] text-center flex items-center justify-center px-3 py-2 text-sm rounded-md"
+                ><el-icon class="stats-card__icon" :size="28">
+                  <Icon icon="ep:bell-filled" /> </el-icon
+                >证书60天橙色预警</div
+              >
+            </div>
           </section>
         </div>
       </div>
     </div>
   </div>
+
+  <Dialog v-model="dialogFileView" title="附件" width="500">
+    <div
+      v-for="(file, index) in fileList"
+      :key="index"
+      class="flex items-center justify-between mt-5">
+      <span class="file-name-text">{{ extractFileName(file) }}</span>
+      <div>
+        <el-button link type="primary" @click="viewFileInfo(file)">
+          <Icon icon="ep:view" class="mr-2px" />查看</el-button
+        >
+        <el-button link type="primary" @click="handleDownload(file)">
+          <Icon icon="ep:download" class="mr-2px" />下载</el-button
+        >
+      </div>
+    </div>
+
+    <template #footer>
+      <div class="dialog-footer mt-10">
+        <el-button type="primary" @click="dialogFileView = false"> 确认 </el-button>
+      </div>
+    </template>
+  </Dialog>
 </template>
 
 <style lang="scss" scoped>
@@ -1922,4 +1995,42 @@ onUnmounted(() => {
     line-height: 14px !important;
   }
 }
+
+/* 过期行的红色背景 - 基础状态 */
+:deep(.el-table__body tr.expired-row > td.el-table__cell) {
+  background-color: #ffe6e6 !important;
+}
+
+/* 过期行 - 鼠标悬浮状态 */
+:deep(.el-table__body tr.expired-row:hover > td.el-table__cell) {
+  background-color: #ffcccc !important;
+}
+
+/* 确保斑马纹不影响过期行 */
+:deep(.el-table__body tr.expired-row.el-table__row--striped > td.el-table__cell) {
+  background-color: #ffe6e6 !important;
+}
+
+:deep(.el-table__body tr.expired-row.el-table__row--striped:hover > td.el-table__cell) {
+  background-color: #ffcccc !important;
+}
+
+/* 预警行的橙色背景 - 基础状态 */
+:deep(.el-table__body tr.alert-warn-row > td.el-table__cell) {
+  background-color: #fff1df !important;
+}
+
+/* 预警行 - 鼠标悬浮状态 */
+:deep(.el-table__body tr.alert-warn-row:hover > td.el-table__cell) {
+  background-color: #ffe2bf !important;
+}
+
+/* 确保斑马纹不影响预警行 */
+:deep(.el-table__body tr.alert-warn-row.el-table__row--striped > td.el-table__cell) {
+  background-color: #fff1df !important;
+}
+
+:deep(.el-table__body tr.alert-warn-row.el-table__row--striped:hover > td.el-table__cell) {
+  background-color: #ffe2bf !important;
+}
 </style>

+ 23 - 1
src/views/pms/stat/rdkb/rd-device-category.vue

@@ -7,14 +7,17 @@ interface DeviceCategoryItem {
   name: string
   count: number
   type: string
+  code?: string | number
 }
 
 interface DeviceCategoryResponseItem {
   className?: string
   count?: number | string
   type?: string
+  code?: string | number
 }
 
+const { push } = useRouter()
 const chartRef = ref<HTMLDivElement>()
 const loading = ref(false)
 const categoryData = ref<DeviceCategoryItem[]>([])
@@ -150,11 +153,13 @@ function getChartOption(): echarts.EChartsOption {
         type: 'bar',
         data: categoryData.value.map((item) => ({
           value: item.count,
+          code: item.code,
           itemStyle: {
             color: item.type === 'top' ? TOP_BAR_COLOR : DEFAULT_BAR_COLOR
           }
         })),
         barWidth: layout.barWidth,
+        cursor: 'pointer',
         showBackground: true,
         backgroundStyle: {
           color: 'rgba(31, 91, 184, 0.08)',
@@ -193,9 +198,25 @@ function initChart() {
   chart = echarts.init(chartRef.value, undefined, {
     renderer: CHART_RENDERER
   })
+  bindChartEvents()
   renderChart()
 }
 
+function bindChartEvents() {
+  chart?.off('click')
+  chart?.on('click', (params) => {
+    const row = categoryData.value[params.dataIndex ?? -1]
+    if (!row?.code) return
+
+    push({
+      name: 'IotDevicePms',
+      query: {
+        code: String(row.code)
+      }
+    })
+  })
+}
+
 function renderChart() {
   chart?.setOption(getChartOption(), true)
 }
@@ -220,7 +241,8 @@ async function getDeviceCategory() {
           res.map((item: DeviceCategoryResponseItem) => ({
             name: item.className || '未知',
             count: Number(item.count ?? 0),
-            type: item.type || ''
+            type: item.type || '',
+            code: item.code
           }))
         ).slice(0, 10)
       : []