소스 검색

fix: 5426 移动端外来人员登记,增加查看生物安全告知书和保密协议功能

johnsen 5 달 전
부모
커밋
f50328469b
4개의 변경된 파일622개의 추가작업 그리고 2개의 파일을 삭제
  1. 1 0
      public/index.html
  2. 0 0
      public/mammoth.browser.min.js
  3. 616 0
      src/components/ibps-file-preview/out-preview.vue
  4. 5 2
      src/views/platform/bpmn/alienRegistration/index.vue

+ 1 - 0
public/index.html

@@ -11,6 +11,7 @@
     <link rel="icon" href="<%= BASE_URL %>favicon.ico">
     <link rel="icon" href="<%= BASE_URL %>favicon.ico">
     <title><%= webpackConfig.name %></title>
     <title><%= webpackConfig.name %></title>
     <script type="text/javascript" src="/word/web-apps/apps/api/documents/api.js"></script>
     <script type="text/javascript" src="/word/web-apps/apps/api/documents/api.js"></script>
+    <script type="text/javascript" src="/mammoth.browser.min.js"></script>
     <style>
     <style>
        *{margin:0;padding:0;list-style:0;-webkit-touch-callout:none;}html{touch-action: manipulation;}
        *{margin:0;padding:0;list-style:0;-webkit-touch-callout:none;}html{touch-action: manipulation;}
     </style>
     </style>

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
public/mammoth.browser.min.js


+ 616 - 0
src/components/ibps-file-preview/out-preview.vue

@@ -0,0 +1,616 @@
+<template>
+  <van-popup v-model="showPopup" class="ibps-fullscreen-popup">
+    <div class="file-preview-container">
+    <!-- 加载状态 -->
+    <div v-if="loading" class="loading-wrapper">
+      <div class="loading-spinner"></div>
+      <p class="loading-text">{{ loadingText }}</p>
+    </div>
+
+    <!-- 错误状态 -->
+    <div v-else-if="error" class="error-wrapper">
+      <div class="error-icon">⚠️</div>
+      <p class="error-text">{{ error }}</p>
+      <button class="retry-btn" @click="loadFile">重试</button>
+    </div>
+
+    <!-- PDF 预览 -->
+    <div v-else-if="fileType === 'pdf'" class="pdf-preview-wrapper">
+      <div class="preview-header">
+        <van-icon name="arrow-left" class="left-icon" @click="agree"/>
+        <span class="file-name">《{{ fileInfo.fileName }}》</span>
+      </div>
+      <div class="pdf-viewer">
+        <iframe
+          v-if="previewUrl"
+          :src="previewUrl"
+          class="pdf-iframe"
+          frameborder="0"
+        ></iframe>
+      </div>
+    </div>
+
+    <!-- Word 预览 -->
+    <div v-else-if="fileType === 'word'" class="word-preview-wrapper">
+      <div class="preview-header">
+        <van-icon name="arrow-left" class="left-icon" @click="agree"/>
+        <span class="file-name">《{{ fileInfo.fileName }}》</span>
+      </div>
+      <div ref="wordViewer" class="word-viewer">
+        <div class="word-content" v-html="wordContent"></div>
+      </div>
+    </div>
+
+    <!-- 不支持的文件类型 -->
+    <div v-else class="unsupported-wrapper">
+      <div class="unsupported-icon">📄</div>
+      <p class="unsupported-text">不支持的文件类型</p>
+      <p class="unsupported-hint">当前仅支持 PDF 和 Word (.docx) 文件</p>
+    </div>
+  </div>
+  </van-popup>
+</template>
+
+<script>
+import axios from 'axios'
+import { SYSTEM_URL, BASE_API } from '@/api/baseUrl'
+
+export default {
+  name: 'FilePreview',
+  props: {
+    value: {
+      type: Boolean,
+      default: false
+    },
+    // 文件信息
+    fileInfo: {
+      type: Object,
+      required: true
+    },
+    // 文件名(可选)
+    fileName: {
+      type: String,
+      default: ''
+    },
+    // 请求配置(用于传递 headers、params 等)
+    requestConfig: {
+      type: Object,
+      default: () => ({})
+    },
+    // 加载提示文本
+    loadingText: {
+      type: String,
+      default: '正在加载文件...'
+    }
+  },
+  data() {
+    return {
+      loading: false,
+      error: null,
+      previewUrl: null,
+      wordContent: '',
+      detectedFileType: null,
+      blobUrl: null,
+      showPopup: false
+    }
+  },
+  computed: {
+    // 实际使用的文件类型
+    fileType() {
+      return this.detectFileTypeFromUrl()
+    }
+  },
+  watch: {
+    value: {
+      handler(val) {
+        console.log(val)
+        if (val) {
+          this.cleanup()
+          this.loadFile()
+        }
+        this.showPopup = val
+      },
+      immediate: true
+    }
+  },
+  beforeDestroy() {
+    this.cleanup()
+  },
+  methods: {
+    agree() {
+      this.$emit('close')
+    },
+    /**
+     * 从 URL 检测文件类型
+     */
+    detectFileTypeFromUrl() {
+      const url = '.' + this.fileInfo.ext
+      if (url.includes('.pdf') || url.includes('pdf')) {
+        return 'pdf'
+      }
+      if (url.includes('.docx') || url.includes('.doc') || url.includes('word')) {
+        return 'word'
+      }
+      return null
+    },
+
+    /**
+     * 加载文件
+     */
+    async loadFile() {
+      this.loading = true
+      this.error = null
+      this.previewUrl = null
+      this.wordContent = ''
+      console.log(1111)
+      try {
+        // 获取文件流
+        const response = await this.fetchFileStream()
+        console.log('response', response)
+        // 检测文件类型
+        const blob = new Blob([response.data], { type: `application/${this.detectFileTypeFromUrl()}` })
+        this.detectedFileType = this.detectFileTypeFromUrl()
+        // 创建 Blob URL
+        this.blobUrl = URL.createObjectURL(blob)
+
+        // 根据文件类型处理预览
+        if (this.detectedFileType === 'pdf') {
+          this.previewUrl = this.blobUrl
+          console.log(this.blobUrl)
+        } else if (this.detectedFileType === 'word') {
+          await this.renderWord(blob)
+        } else {
+          this.error = '不支持的文件类型,仅支持 PDF 和 Word (.docx) 文件'
+        }
+      } catch (err) {
+        console.error('文件加载失败:', err)
+        this.error = err.message || '文件加载失败,请稍后重试'
+      } finally {
+        this.loading = false
+      }
+    },
+
+    /**
+     * 获取文件流
+     */
+    async fetchFileStream() {
+      const url =
+      BASE_API() +
+        SYSTEM_URL() +
+        '/file/downloadFileStream?attachmentId=' +
+        this.fileInfo.id +
+        '&fileType=' +
+        this.fileInfo.ext
+      const config = {
+        method: 'GET',
+        url: url,
+        responseType: 'blob', // 重要:设置为 blob 以接收流数据
+        ...this.requestConfig
+      }
+
+      const response = await axios(config)
+      return response
+    },
+
+    /**
+     * 检测文件类型
+     */
+    async detectFileType(contentType, data) {
+      // 从 Content-Type 检测
+      if (contentType.includes('application/pdf') || contentType.includes('pdf')) {
+        return 'pdf'
+      }
+      if (contentType.includes('application/vnd.openxmlformats-officedocument.wordprocessingml.document') ||
+          contentType.includes('application/msword') ||
+          contentType.includes('word')) {
+        return 'word'
+      }
+
+      // 从文件内容检测(检查文件头)
+      if (data instanceof Blob) {
+        try {
+          // 读取文件前几个字节来检测文件类型
+          const arrayBuffer = await data.slice(0, 8).arrayBuffer()
+          const uint8Array = new Uint8Array(arrayBuffer)
+
+          // PDF 文件头:%PDF
+          if (uint8Array[0] === 0x25 && uint8Array[1] === 0x50 && uint8Array[2] === 0x44 && uint8Array[3] === 0x46) {
+            return 'pdf'
+          }
+
+          // DOCX 文件头:PK (ZIP 格式,因为 DOCX 是基于 ZIP 的)
+          // DOCX 文件是 ZIP 格式,前两个字节是 0x50 0x4B (PK)
+          if (uint8Array[0] === 0x50 && uint8Array[1] === 0x4B) {
+            // 进一步检查是否是 DOCX(检查 ZIP 中的 [Content_Types].xml)
+            // 这里简化处理,如果 URL 包含 docx 或 word,则认为是 word
+            const urlType = this.detectFileTypeFromUrl()
+            if (urlType === 'word') {
+              return 'word'
+            }
+            // 否则可能是其他 ZIP 格式文件,默认返回 null
+          }
+        } catch (err) {
+          console.warn('文件类型检测失败:', err)
+        }
+      }
+
+      // 如果无法检测,使用 URL 检测
+      const urlType = this.detectFileTypeFromUrl()
+      if (urlType) {
+        return urlType
+      }
+
+      // 如果仍然无法检测,根据文件大小和 Content-Type 做最后判断
+      // 默认返回 null,让调用方处理
+      return null
+    },
+
+    /**
+     * 渲染 Word 文档
+     */
+    async renderWord(blob) {
+      try {
+        // 动态加载 mammoth.js
+        if (!window.mammoth) {
+          await this.loadMammoth()
+        }
+
+        // 将 Blob 转换为 ArrayBuffer
+        const arrayBuffer = await blob.arrayBuffer()
+
+        // 使用 mammoth 转换为 HTML
+        const result = await window.mammoth.convertToHtml(
+          { arrayBuffer },
+          {
+            styleMap: [
+              "p[style-name='Heading 1'] => h1:fresh",
+              "p[style-name='Heading 2'] => h2:fresh",
+              "p[style-name='Heading 3'] => h3:fresh"
+            ]
+          }
+        )
+
+        this.wordContent = result.value
+
+        // 如果有警告,可以在这里处理
+        if (result.messages && result.messages.length > 0) {
+          console.warn('Word 转换警告:', result.messages)
+        }
+      } catch (err) {
+        console.error('Word 渲染失败:', err)
+        throw new Error('Word 文件解析失败,请确保文件格式正确')
+      }
+    },
+
+    /**
+     * 动态加载 mammoth.js
+     */
+    loadMammoth() {
+      return new Promise((resolve, reject) => {
+        if (window.mammoth) {
+          resolve()
+          return
+        }
+
+        const script = document.createElement('script')
+        script.src = 'https://cdn.jsdelivr.net/npm/mammoth@1.6.0/mammoth.browser.min.js'
+        script.onload = () => {
+          if (window.mammoth) {
+            resolve()
+          } else {
+            reject(new Error('mammoth.js 加载失败'))
+          }
+        }
+        script.onerror = () => {
+          reject(new Error('无法加载 mammoth.js,请检查网络连接'))
+        }
+        document.head.appendChild(script)
+      })
+    },
+
+    /**
+     * 下载文件
+     */
+    downloadFile() {
+      if (!this.blobUrl) return
+
+      const link = document.createElement('a')
+      link.href = this.blobUrl
+      link.download = this.fileName || 'download'
+      document.body.appendChild(link)
+      link.click()
+      document.body.removeChild(link)
+    },
+
+    /**
+     * 在新窗口打开
+     */
+    openInNewTab() {
+      if (!this.blobUrl) return
+      window.open(this.blobUrl, '_blank')
+    },
+
+    /**
+     * 清理资源
+     */
+    cleanup() {
+      if (this.blobUrl) {
+        URL.revokeObjectURL(this.blobUrl)
+        this.blobUrl = null
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.left-icon {
+  position: absolute;
+  left: 8px;
+}
+.file-preview-container {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  background: #f5f5f5;
+  border-radius: 8px;
+  overflow: hidden;
+}
+
+/* 加载状态 */
+.loading-wrapper {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 40px;
+}
+
+.loading-spinner {
+  width: 50px;
+  height: 50px;
+  border: 4px solid #e0e0e0;
+  border-top-color: #2196f3;
+  border-radius: 50%;
+  animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+  to {
+    transform: rotate(360deg);
+  }
+}
+
+.loading-text {
+  margin-top: 20px;
+  color: #666;
+  font-size: 14px;
+}
+
+/* 错误状态 */
+.error-wrapper {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 40px;
+}
+
+.error-icon {
+  font-size: 48px;
+  margin-bottom: 16px;
+}
+
+.error-text {
+  color: #f44336;
+  font-size: 16px;
+  margin-bottom: 20px;
+  text-align: center;
+}
+
+.retry-btn {
+  padding: 10px 24px;
+  background: #2196f3;
+  color: white;
+  border: none;
+  border-radius: 4px;
+  cursor: pointer;
+  font-size: 14px;
+  transition: background 0.3s;
+}
+
+.retry-btn:hover {
+  background: #1976d2;
+}
+
+/* 预览头部 */
+.preview-header {
+  text-align: center;
+  padding: 12px 16px;
+  background: white;
+  border-bottom: 1px solid #e0e0e0;
+}
+
+.file-name {
+  font-size: 16px;
+  font-weight: 500;
+  color: #333;
+  flex: 1;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  text-align: center;
+}
+
+.header-actions {
+  display: flex;
+  gap: 8px;
+}
+
+.action-btn {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  padding: 6px 12px;
+  background: #f5f5f5;
+  border: 1px solid #e0e0e0;
+  border-radius: 4px;
+  cursor: pointer;
+  font-size: 14px;
+  color: #666;
+  transition: all 0.3s;
+}
+
+.action-btn:hover {
+  background: #e0e0e0;
+  color: #333;
+}
+
+.action-btn span {
+  font-size: 12px;
+}
+
+/* PDF 预览 */
+.pdf-preview-wrapper {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+
+.pdf-viewer {
+  flex: 1;
+  overflow: hidden;
+  background: #525252;
+}
+
+.pdf-iframe {
+  width: 100%;
+  height: 100%;
+  border: none;
+}
+
+/* Word 预览 */
+.word-preview-wrapper {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+
+.word-viewer {
+  flex: 1;
+  overflow-y: auto;
+  background: white;
+  padding: 24px;
+}
+
+.word-content {
+  max-width: 800px;
+  margin: 0 auto;
+  line-height: 1.6;
+  color: #333;
+}
+
+.word-content >>> h1,
+.word-content >>> h2,
+.word-content >>> h3 {
+  margin-top: 24px;
+  margin-bottom: 16px;
+  font-weight: 600;
+}
+
+.word-content >>> h1 {
+  font-size: 32px;
+  border-bottom: 2px solid #e0e0e0;
+  padding-bottom: 8px;
+}
+
+.word-content >>> h2 {
+  font-size: 24px;
+}
+
+.word-content >>> h3 {
+  font-size: 20px;
+}
+
+.word-content >>> p {
+  margin-bottom: 12px;
+}
+
+.word-content >>> ul,
+.word-content >>> ol {
+  margin-left: 24px;
+  margin-bottom: 12px;
+}
+
+.word-content >>> table {
+  width: 100%;
+  border-collapse: collapse;
+  margin: 16px 0;
+}
+
+.word-content >>> table td,
+.word-content >>> table th {
+  border: 1px solid #e0e0e0;
+  padding: 8px;
+}
+
+.word-content >>> table th {
+  background: #f5f5f5;
+  font-weight: 600;
+}
+
+/* 不支持的文件类型 */
+.unsupported-wrapper {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 40px;
+}
+
+.unsupported-icon {
+  font-size: 64px;
+  margin-bottom: 16px;
+}
+
+.unsupported-text {
+  font-size: 18px;
+  color: #666;
+  margin-bottom: 8px;
+}
+
+.unsupported-hint {
+  font-size: 14px;
+  color: #999;
+}
+
+/* 移动端适配 */
+@media (max-width: 768px) {
+  .preview-header {
+    padding: 10px 12px;
+  }
+
+  .file-name {
+    font-size: 14px;
+  }
+
+  .action-btn {
+    padding: 6px 10px;
+    font-size: 12px;
+  }
+
+  .word-viewer {
+    padding: 16px;
+  }
+
+  .word-content {
+    max-width: 100%;
+  }
+}
+</style>

+ 5 - 2
src/views/platform/bpmn/alienRegistration/index.vue

@@ -174,15 +174,17 @@ class="link"
     </van-popup>
     </van-popup>
     <!--预览 -->
     <!--预览 -->
     <ibps-file-preview
     <ibps-file-preview
+      v-if="currentFile"
       v-model="showPreviewPopup"
       v-model="showPreviewPopup"
-      :file="currentFile"
+      :file-type="currentFile.ext"
+      :file-info="currentFile"
       @close="close"
       @close="close"
     />
     />
   </div>
   </div>
 </template>
 </template>
 <script>
 <script>
 import { query, save, getRegistrationOutsidersFileConfig } from '@/api/platform/feature/alienRegistration'
 import { query, save, getRegistrationOutsidersFileConfig } from '@/api/platform/feature/alienRegistration'
-import IbpsFilePreview from '@/components/ibps-file-preview'
+import IbpsFilePreview from '@/components/ibps-file-preview/out-preview.vue'
 import ActionUtils from '@/utils/action'
 import ActionUtils from '@/utils/action'
 export default {
 export default {
   components: {
   components: {
@@ -275,6 +277,7 @@ export default {
       this.showPreviewPopup = false
       this.showPreviewPopup = false
     },
     },
     openFileHandler(file) {
     openFileHandler(file) {
+      console.log('111111')
       this.currentFile = file
       this.currentFile = file
       this.showPreviewPopup = true
       this.showPreviewPopup = true
     },
     },

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.