yanghao пре 1 дан
родитељ
комит
931ea7f256

+ 28 - 0
src/api/pms/qhse/index.ts

@@ -259,3 +259,31 @@ export const IotApprovalApi = {
     return await request.get({ url: `/rq/iot-accident-report/approval`, params })
   }
 }
+
+// 隐患排查
+export const IotHiddenApi = {
+  // 获得隐患排查分页
+  getHiddenList: async (params) => {
+    return await request.get({ url: `/rq/iot-hazard/page`, params })
+  },
+  // 删除隐患排查
+  deleteHidden: async (id) => {
+    return await request.delete({ url: `/rq/iot-hazard/delete?id=` + id })
+  },
+  // 添加隐患排查
+  createHidden: async (data) => {
+    return await request.post({ url: `/rq/iot-hazard/create`, data })
+  },
+  // 修改隐患排查
+  updateHidden: async (data) => {
+    return await request.put({ url: `/rq/iot-hazard/update`, data })
+  },
+  // 导出隐患排查 Excel
+  exportHidden: async (params) => {
+    return await request.download({ url: `/rq/iot-hazard/export-excel`, params })
+  },
+  // 整改
+  rectifyHidden: async (data) => {
+    return await request.put({ url: `/rq/iot-hazard/rectify`, data })
+  }
+}

+ 5 - 0
src/components/FilePreview/index.ts

@@ -0,0 +1,5 @@
+import FilePreviewDialog from './src/FilePreviewDialog.vue'
+
+export { FilePreviewDialog }
+export default FilePreviewDialog
+

+ 306 - 0
src/components/FilePreview/src/FilePreviewDialog.vue

@@ -0,0 +1,306 @@
+<template>
+  <el-dialog
+    v-model="visible"
+    :title="title"
+    :width="width"
+    append-to-body
+    destroy-on-close
+    class="file-preview-dialog"
+  >
+    <div class="toolbar">
+      <div class="left">
+        <el-select
+          v-if="normalizedUrls.length > 1"
+          v-model="activeUrl"
+          filterable
+          class="file-select"
+          placeholder="选择文件"
+        >
+          <el-option
+            v-for="(u, idx) in normalizedUrls"
+            :key="u + idx"
+            :label="guessNameFromUrl(u)"
+            :value="u"
+          />
+        </el-select>
+        <div v-else class="file-name" :title="activeName">
+          {{ activeName }}
+        </div>
+        <el-tag size="small" type="info" class="file-type">{{ activeExt || 'file' }}</el-tag>
+      </div>
+
+      <div class="right">
+        <el-button :disabled="!activeUrl" type="primary" plain @click="download">
+          <Icon icon="ep:download" class="mr-5px" />
+          下载
+        </el-button>
+      </div>
+    </div>
+
+    <div class="content" :style="{ height }">
+      <el-empty v-if="!activeUrl" description="暂无可预览文件" />
+
+      <template v-else>
+        <el-alert
+          v-if="previewHint"
+          :title="previewHint"
+          type="warning"
+          show-icon
+          :closable="false"
+          class="hint"
+        />
+
+        <!-- 图片 -->
+        <div v-if="activeKind === 'image'" class="stage">
+          <el-image
+            :src="activeUrl"
+            fit="contain"
+            :preview-src-list="[activeUrl]"
+            preview-teleported
+            class="img"
+          />
+        </div>
+
+        <!-- PDF -->
+        <div v-else-if="activeKind === 'pdf'" class="stage">
+          <iframe class="frame" :src="pdfIframeSrc" title="PDF Preview" frameborder="0"></iframe>
+        </div>
+
+        <!-- Word -->
+        <div v-else-if="activeKind === 'word'" class="stage">
+          <iframe
+            v-if="officeIframeSrc"
+            class="frame"
+            :src="officeIframeSrc"
+            title="Office Preview"
+            frameborder="0"
+          ></iframe>
+          <el-empty v-else description="该文件无法直接预览" />
+        </div>
+
+        <!-- 其它 -->
+        <div v-else class="stage">
+          <el-empty description="暂不支持该格式预览" />
+        </div>
+      </template>
+    </div>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { computed, ref, watch } from 'vue'
+
+defineOptions({ name: 'FilePreviewDialog' })
+
+type OfficeViewer = 'microsoft'
+
+const props = withDefaults(
+  defineProps<{
+    modelValue: boolean
+    title?: string
+    width?: string | number
+    height?: string
+    url?: string
+    urls?: string[]
+    name?: string
+    useOfficeViewer?: boolean
+    officeViewer?: OfficeViewer
+  }>(),
+  {
+    title: '文件预览',
+    width: '1100px',
+    height: '72vh',
+    url: '',
+    urls: () => [],
+    name: '',
+    useOfficeViewer: true,
+    officeViewer: 'microsoft'
+  }
+)
+
+const emit = defineEmits<{
+  (e: 'update:modelValue', v: boolean): void
+}>()
+
+const visible = computed({
+  get: () => props.modelValue,
+  set: (v: boolean) => emit('update:modelValue', v)
+})
+
+const normalizedUrls = computed(() => {
+  const list = [...(props.urls || []), props.url].filter(Boolean) as string[]
+  const uniq: string[] = []
+  for (const u of list) {
+    if (!uniq.includes(u)) uniq.push(u)
+  }
+  return uniq
+})
+
+const activeUrl = ref<string>('')
+
+watch(
+  () => normalizedUrls.value,
+  (list) => {
+    activeUrl.value = list[0] || ''
+  },
+  { immediate: true }
+)
+
+const activeName = computed(() => {
+  if (!activeUrl.value) return ''
+  if (props.name) return props.name
+  return guessNameFromUrl(activeUrl.value)
+})
+
+const activeExt = computed(() => getFileExt(activeName.value || activeUrl.value))
+
+type Kind = 'image' | 'pdf' | 'word' | 'other'
+const activeKind = computed<Kind>(() => {
+  const ext = activeExt.value
+  if (['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp', 'svg'].includes(ext)) return 'image'
+  if (ext === 'pdf') return 'pdf'
+  if (ext === 'doc' || ext === 'docx') return 'word'
+  return 'other'
+})
+
+const isHttpUrl = (u: string) => /^https?:\/\//i.test(u)
+
+const officeIframeSrc = computed(() => {
+  if (!props.useOfficeViewer) return ''
+  if (!activeUrl.value) return ''
+  if (!isHttpUrl(activeUrl.value)) return ''
+  if (props.officeViewer !== 'microsoft') return ''
+  return `https://view.officeapps.live.com/op/embed.aspx?src=${encodeURIComponent(activeUrl.value)}`
+})
+
+const pdfIframeSrc = computed(() => {
+  if (!activeUrl.value) return ''
+  // Chrome 内置 PDF 预览支持 #toolbar=0
+  return `${activeUrl.value}#toolbar=0&navpanes=0&scrollbar=0`
+})
+
+const previewHint = computed(() => {
+  if (!activeUrl.value) return ''
+  if (activeKind.value === 'word' && !officeIframeSrc.value) {
+    return 'Word 预览需要可被 Office 在线服务访问的完整 URL(通常要求公网/无鉴权),否则建议后端转 PDF 再预览。'
+  }
+  return ''
+})
+
+const openNewTab = () => {
+  if (!activeUrl.value) return
+  window.open(activeUrl.value, '_blank', 'noopener,noreferrer')
+}
+
+const download = () => {
+  if (!activeUrl.value) return
+  const a = document.createElement('a')
+  a.href = activeUrl.value
+  a.download = activeName.value || ''
+  a.target = '_blank'
+  a.rel = 'noopener noreferrer'
+  document.body.appendChild(a)
+  a.click()
+  a.remove()
+}
+
+function getFileExt(input: string) {
+  const clean = input.split('?')[0].split('#')[0]
+  const dot = clean.lastIndexOf('.')
+  if (dot < 0) return ''
+  return clean.substring(dot + 1).toLowerCase()
+}
+
+function guessNameFromUrl(u: string) {
+  try {
+    const url = new URL(u, window.location.origin)
+    const name = url.pathname.split('/').filter(Boolean).pop() || ''
+    return decodeURIComponent(name) || u
+  } catch {
+    const clean = u.split('?')[0].split('#')[0]
+    const name = clean.split('/').filter(Boolean).pop() || clean
+    try {
+      return decodeURIComponent(name)
+    } catch {
+      return name
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.file-preview-dialog {
+  :deep(.el-dialog__body) {
+    padding-top: 10px;
+  }
+}
+
+.toolbar {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px;
+  padding: 0 0 10px;
+  border-bottom: 1px solid var(--el-border-color-lighter);
+}
+
+.left {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  min-width: 0;
+}
+
+.file-select {
+  width: min(520px, 48vw);
+}
+
+.file-name {
+  max-width: min(520px, 48vw);
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  color: var(--el-text-color-primary);
+  font-weight: 600;
+}
+
+.file-type {
+  flex: none;
+}
+
+.right {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  flex: none;
+}
+
+.content {
+  margin-top: 12px;
+  position: relative;
+}
+
+.hint {
+  margin-bottom: 10px;
+}
+
+.stage {
+  height: 100%;
+  border: 1px solid var(--el-border-color-lighter);
+  border-radius: 10px;
+  overflow: hidden;
+  background: linear-gradient(180deg, rgba(0, 0, 0, 0.02), rgba(0, 0, 0, 0.01));
+}
+
+.img {
+  width: 100%;
+  height: 100%;
+}
+
+.frame {
+  width: 100%;
+  height: 100%;
+  border: 0;
+  background: #fff;
+}
+</style>

+ 2 - 1
src/utils/dict.ts

@@ -321,7 +321,8 @@ export enum DICT_TYPE {
   PERSON_CERT = 'person_cert',
   ORG_CERT = 'org_cert',
   DANGER_GRADE = 'danger_grade',
-  ACCIDENT_REPORT_STATUS = 'accident_report_status'
+  ACCIDENT_REPORT_STATUS = 'accident_report_status',
+  QHSE_HAZARD_STATUS = 'qhse_hazard_status'
 }
 
 export function realValue(type: any, value: string) {

+ 1 - 1
src/views/pms/qhse/certificate.vue

@@ -20,7 +20,7 @@
               v-model="queryParams.classify"
               placeholder="证书类别"
               clearable
-              class="!w-240px"
+              class="!w-180px"
             >
               <el-option
                 v-for="dict in getStrDictOptions(DICT_TYPE.PERSON_CERT).concat(

Разлика између датотеке није приказан због своје велике величине
+ 455 - 481
src/views/pms/qhse/safety/index.vue


Неке датотеке нису приказане због велике количине промена