| | |
| | | <template> |
| | | <div> |
| | | <el-dialog title="查看附件" |
| | | v-model="dialogVisitable" width="800px" @close="cancel"> |
| | | <div class="upload-container"> |
| | | <!-- 生产前 --> |
| | | <div class="form-container"> |
| | | <div class="title">生产前</div> |
| | | |
| | | <!-- 图片列表 --> |
| | | <div style="display: flex; flex-wrap: wrap;"> |
| | | <img v-for="(item, index) in beforeProductionImgs" :key="index" |
| | | @click="showMedia(beforeProductionImgs, index, 'image')" |
| | | :src="item" style="max-width: 100px; height: 100px; margin: 5px;" alt=""> |
| | | <el-dialog title="巡检详情" |
| | | v-model="dialogVisitable" width="700px" @close="cancel"> |
| | | <div class="detail-container"> |
| | | <!-- 基本信息 --> |
| | | <div class="info-section"> |
| | | <div class="section-title">基本信息</div> |
| | | <el-descriptions :column="2" border> |
| | | <el-descriptions-item label="设备名称">{{ rowData.taskName || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="巡检地点">{{ rowData.inspectionLocation || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="执行巡检人">{{ rowData.inspector || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="巡检时间">{{ rowData.dateStr || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="巡检状态"> |
| | | <el-tag v-if="rowData.inspectionStatus === 1" type="success" size="small">正常</el-tag> |
| | | <el-tag v-else-if="rowData.inspectionStatus === 2" type="danger" size="small">异常</el-tag> |
| | | <el-tag v-else size="small">未巡检</el-tag> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="登记人">{{ rowData.registrant || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="验收状态"> |
| | | <el-tag v-if="rowData.acceptStatus === 1" type="success" size="small">已通过</el-tag> |
| | | <el-tag v-else-if="rowData.acceptStatus === 2" type="danger" size="small">已退回</el-tag> |
| | | <el-tag v-else-if="rowData.inspectionStatus > 0" type="warning" size="small">待验收</el-tag> |
| | | <span v-else>--</span> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="验收人">{{ rowData.inspectionAcceptor || '-' }}</el-descriptions-item> |
| | | </el-descriptions> |
| | | </div> |
| | | |
| | | <!-- 视频列表 --> |
| | | <div style="display: flex; flex-wrap: wrap;"> |
| | | <div |
| | | v-for="(videoUrl, index) in beforeProductionVideos" |
| | | :key="index" |
| | | @click="showMedia(beforeProductionVideos, index, 'video')" |
| | | style="position: relative; margin: 10px; cursor: pointer;" |
| | | > |
| | | <div style="width: 160px; height: 90px; background-color: #333; display: flex; align-items: center; justify-content: center;"> |
| | | <img src="@/assets/images/video.png" alt="播放" style="width: 30px; height: 30px; opacity: 0.8;" /> |
| | | <!-- 异常描述 --> |
| | | <div v-if="rowData.inspectionStatus === 2" class="info-section"> |
| | | <div class="section-title">异常描述</div> |
| | | <div class="exception-content"> |
| | | {{ rowData.inspectionRemark || '无' }} |
| | | </div> |
| | | <div style="text-align: center; font-size: 12px; color: #666;">点击播放</div> |
| | | </div> |
| | | |
| | | <!-- 附件列表 --> |
| | | <div class="info-section"> |
| | | <div class="section-title">附件 ({{ attachmentList.length }}个)</div> |
| | | <div v-if="attachmentList.length > 0" class="attachment-list"> |
| | | <div v-for="(file, index) in attachmentList" :key="index" class="attachment-item"> |
| | | <div class="attachment-preview" @click="previewFile(file, index)"> |
| | | <img v-if="isImage(file)" :src="getFileUrl(file)" alt="附件" /> |
| | | <video v-else-if="isVideo(file)" :src="getFileUrl(file)"></video> |
| | | <div v-else class="file-icon"> |
| | | <i class="el-icon-document"></i> |
| | | </div> |
| | | </div> |
| | | <div class="attachment-info"> |
| | | <span class="file-name">{{ file.originalFilename || file.name || '附件' }}</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div v-else class="empty-attachment"> |
| | | 暂无附件 |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 生产后 --> |
| | | <div class="form-container"> |
| | | <div class="title">生产后</div> |
| | | |
| | | <!-- 图片列表 --> |
| | | <div style="display: flex; flex-wrap: wrap;"> |
| | | <img v-for="(item, index) in afterProductionImgs" :key="index" |
| | | @click="showMedia(afterProductionImgs, index, 'image')" |
| | | :src="item" style="max-width: 100px; height: 100px; margin: 5px;" alt=""> |
| | | </div> |
| | | |
| | | <!-- 视频列表 --> |
| | | <div style="display: flex; flex-wrap: wrap;"> |
| | | <div |
| | | v-for="(videoUrl, index) in afterProductionVideos" |
| | | :key="index" |
| | | @click="showMedia(afterProductionVideos, index, 'video')" |
| | | style="position: relative; margin: 10px; cursor: pointer;" |
| | | > |
| | | <div style="width: 160px; height: 90px; background-color: #333; display: flex; align-items: center; justify-content: center;"> |
| | | <img src="@/assets/images/video.png" alt="播放" style="width: 30px; height: 30px; opacity: 0.8;" /> |
| | | </div> |
| | | <div style="text-align: center; font-size: 12px; color: #666;">点击播放</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 生产问题 --> |
| | | <div class="form-container"> |
| | | <div class="title">生产问题</div> |
| | | |
| | | <!-- 图片列表 --> |
| | | <div style="display: flex; flex-wrap: wrap;"> |
| | | <img v-for="(item, index) in productionIssuesImgs" :key="index" |
| | | @click="showMedia(productionIssuesImgs, index, 'image')" |
| | | :src="item" style="max-width: 100px; height: 100px; margin: 5px;" alt=""> |
| | | </div> |
| | | |
| | | <!-- 视频列表 --> |
| | | <div style="display: flex; flex-wrap: wrap;"> |
| | | <div |
| | | v-for="(videoUrl, index) in productionIssuesVideos" |
| | | :key="index" |
| | | @click="showMedia(productionIssuesVideos, index, 'video')" |
| | | style="position: relative; margin: 10px; cursor: pointer;" |
| | | > |
| | | <div style="width: 160px; height: 90px; background-color: #333; display: flex; align-items: center; justify-content: center;"> |
| | | <img src="@/assets/images/video.png" alt="播放" style="width: 30px; height: 30px; opacity: 0.8;" /> |
| | | </div> |
| | | <div style="text-align: center; font-size: 12px; color: #666;">点击播放</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <template #footer> |
| | | <el-button @click="cancel">关闭</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | |
| | | <!-- 统一媒体查看器 --> |
| | | <div v-if="isMediaViewerVisible" class="media-viewer-overlay" @click.self="closeMediaViewer"> |
| | | <div class="media-viewer-content" @click.stop> |
| | | <!-- 图片 --> |
| | | <!-- 图片预览 --> |
| | | <vue-easy-lightbox |
| | | v-if="mediaType === 'image'" |
| | | :visible="isMediaViewerVisible" |
| | | :imgs="mediaList" |
| | | :index="currentMediaIndex" |
| | | @hide="closeMediaViewer" |
| | | v-if="showLightbox" |
| | | :visible="showLightbox" |
| | | :imgs="previewImages" |
| | | :index="previewIndex" |
| | | @hide="closeLightbox" |
| | | ></vue-easy-lightbox> |
| | | |
| | | <!-- 视频 --> |
| | | <div v-else-if="mediaType === 'video'" style="position: relative;"> |
| | | <!-- 视频预览 --> |
| | | <el-dialog v-if="showVideoDialog" v-model="showVideoDialog" title="视频预览" width="800px" append-to-body> |
| | | <video |
| | | :src="mediaList[currentMediaIndex]" |
| | | autoplay |
| | | ref="videoPlayer" |
| | | :src="videoUrl" |
| | | controls |
| | | style="max-width: 90vw; max-height: 80vh;" |
| | | /> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | autoplay |
| | | style="width: 100%; max-height: 60vh;" |
| | | ></video> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref } from 'vue'; |
| | | import { ref, getCurrentInstance } from 'vue'; |
| | | import VueEasyLightbox from 'vue-easy-lightbox'; |
| | | |
| | | const { proxy } = getCurrentInstance(); |
| | | |
| | | // 控制弹窗显示 |
| | | const dialogVisitable = ref(false); |
| | | |
| | | // 图片数组 |
| | | const beforeProductionImgs = ref([]); |
| | | const afterProductionImgs = ref([]); |
| | | const productionIssuesImgs = ref([]); |
| | | // 行数据 |
| | | const rowData = ref({}); |
| | | |
| | | // 视频数组 |
| | | const beforeProductionVideos = ref([]); |
| | | const afterProductionVideos = ref([]); |
| | | const productionIssuesVideos = ref([]); |
| | | // 附件列表 |
| | | const attachmentList = ref([]); |
| | | |
| | | // 媒体查看器状态 |
| | | const isMediaViewerVisible = ref(false); |
| | | const currentMediaIndex = ref(0); |
| | | const mediaList = ref([]); // 存储当前要查看的媒体列表(含图片和视频对象) |
| | | const mediaType = ref('image'); // image | video |
| | | // 图片预览 |
| | | const showLightbox = ref(false); |
| | | const previewImages = ref([]); |
| | | const previewIndex = ref(0); |
| | | |
| | | // 视频预览 |
| | | const showVideoDialog = ref(false); |
| | | const videoUrl = ref(''); |
| | | |
| | | const javaApi = proxy.javaApi; |
| | | |
| | | // 处理 URL:将 Windows 路径转换为可访问的 URL |
| | | // 处理文件URL |
| | | function processFileUrl(fileUrl) { |
| | | if (!fileUrl) return ''; |
| | | |
| | | // 如果 URL 是 Windows 路径格式(包含反斜杠),需要转换 |
| | | if (fileUrl && fileUrl.indexOf('\\') > -1) { |
| | | // 查找 uploads 关键字的位置,从那里开始提取相对路径 |
| | | const uploadsIndex = fileUrl.toLowerCase().indexOf('uploads'); |
| | | if (uploadsIndex > -1) { |
| | | // 从 uploads 开始提取路径,并将反斜杠替换为正斜杠 |
| | | const relativePath = fileUrl.substring(uploadsIndex).replace(/\\/g, '/'); |
| | | fileUrl = '/' + relativePath; |
| | | } else { |
| | | // 如果没有找到 uploads,提取最后一个目录和文件名 |
| | | const parts = fileUrl.split('\\'); |
| | | const fileName = parts[parts.length - 1]; |
| | | fileUrl = '/uploads/' + fileName; |
| | | } |
| | | } |
| | | |
| | | // 确保所有非 http 开头的 URL 都拼接 baseUrl |
| | | if (fileUrl && !fileUrl.startsWith('http')) { |
| | | // 确保路径以 / 开头 |
| | | if (!fileUrl.startsWith('/')) { |
| | | fileUrl = '/' + fileUrl; |
| | | } |
| | | // 拼接 baseUrl |
| | | fileUrl = javaApi + fileUrl; |
| | | } |
| | | |
| | | return fileUrl; |
| | | } |
| | | |
| | | // 处理每一类数据:分离图片和视频 |
| | | function processItems(items) { |
| | | const images = []; |
| | | const videos = []; |
| | | |
| | | // 检查 items 是否存在且为数组 |
| | | if (!items || !Array.isArray(items)) { |
| | | return { images, videos }; |
| | | // 获取文件访问URL |
| | | function getFileUrl(file) { |
| | | // 优先使用 link 字段 |
| | | let url = file?.link; |
| | | if (!url) { |
| | | url = file?.url || file?.downloadUrl || file?.tempPath || ''; |
| | | } |
| | | return processFileUrl(url); |
| | | } |
| | | |
| | | items.forEach(item => { |
| | | if (!item || !item.link) return; |
| | | |
| | | const fileUrl = processFileUrl(item.link); |
| | | // if (!item || !item.url) return; |
| | | |
| | | // // 处理文件 URL |
| | | // const fileUrl = processFileUrl(item.url); |
| | | |
| | | // 根据文件扩展名判断是图片还是视频 |
| | | const urlLower = fileUrl.toLowerCase(); |
| | | if (urlLower.match(/\.(jpg|jpeg|png|gif|bmp|webp)$/)) { |
| | | images.push(fileUrl); |
| | | } else if (urlLower.match(/\.(mp4|avi|mov|wmv|flv|mkv|webm)$/)) { |
| | | videos.push(fileUrl); |
| | | } else if (item.contentType) { |
| | | // 如果有 contentType,使用 contentType 判断 |
| | | if (item.contentType.startsWith('image/')) { |
| | | images.push(fileUrl); |
| | | } else if (item.contentType.startsWith('video/')) { |
| | | videos.push(fileUrl); |
| | | } |
| | | } |
| | | }); |
| | | |
| | | return { images, videos }; |
| | | // 判断是否为图片 |
| | | function isImage(file) { |
| | | const name = file?.originalFilename || file?.bucketFilename || file?.name || ''; |
| | | const ext = name.split('.').pop()?.toLowerCase(); |
| | | return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(ext); |
| | | } |
| | | |
| | | // 打开弹窗并加载数据 |
| | | const openDialog = async (row) => { |
| | | // 使用正确的字段名:commonFileListBefore, commonFileListAfter |
| | | // productionIssues 可能不存在,使用空数组 |
| | | const { images: beforeImgs, videos: beforeVids } = processItems(row.commonFileListBefore || []); |
| | | const { images: afterImgs, videos: afterVids } = processItems(row.commonFileListAfter || []); |
| | | const { images: issueImgs, videos: issueVids } = processItems(row.productionIssues || []); |
| | | // 判断是否为视频 |
| | | function isVideo(file) { |
| | | const name = file?.originalFilename || file?.bucketFilename || file?.name || ''; |
| | | const ext = name.split('.').pop()?.toLowerCase(); |
| | | return ['mp4', 'avi', 'mov', 'wmv', 'flv', 'mkv', 'webm'].includes(ext); |
| | | } |
| | | |
| | | beforeProductionImgs.value = beforeImgs; |
| | | beforeProductionVideos.value = beforeVids; |
| | | // 预览文件 |
| | | function previewFile(file, index) { |
| | | if (isImage(file)) { |
| | | // 图片预览 |
| | | previewImages.value = attachmentList.value |
| | | .filter(f => isImage(f)) |
| | | .map(f => getFileUrl(f)); |
| | | previewIndex.value = previewImages.value.indexOf(getFileUrl(file)); |
| | | showLightbox.value = true; |
| | | } else if (isVideo(file)) { |
| | | // 视频预览 |
| | | videoUrl.value = getFileUrl(file); |
| | | showVideoDialog.value = true; |
| | | } |
| | | } |
| | | |
| | | afterProductionImgs.value = afterImgs; |
| | | afterProductionVideos.value = afterVids; |
| | | // 关闭图片预览 |
| | | function closeLightbox() { |
| | | showLightbox.value = false; |
| | | previewImages.value = []; |
| | | previewIndex.value = 0; |
| | | } |
| | | |
| | | productionIssuesImgs.value = issueImgs; |
| | | productionIssuesVideos.value = issueVids; |
| | | // 打开弹窗 |
| | | const openDialog = (row) => { |
| | | rowData.value = { ...row }; |
| | | |
| | | // 收集所有附件 |
| | | let files = []; |
| | | if (row?.commonFileList) { |
| | | files = files.concat(row.commonFileList); |
| | | } |
| | | if (row?.commonFileListBefore) { |
| | | files = files.concat(row.commonFileListBefore); |
| | | } |
| | | if (row?.commonFileListAfter) { |
| | | files = files.concat(row.commonFileListAfter); |
| | | } |
| | | attachmentList.value = files; |
| | | |
| | | dialogVisitable.value = true; |
| | | }; |
| | | |
| | | // 显示媒体(图片 or 视频) |
| | | function showMedia(mediaArray, index, type) { |
| | | mediaList.value = mediaArray; |
| | | currentMediaIndex.value = index; |
| | | mediaType.value = type; |
| | | isMediaViewerVisible.value = true; |
| | | } |
| | | |
| | | // 关闭媒体查看器 |
| | | function closeMediaViewer() { |
| | | isMediaViewerVisible.value = false; |
| | | mediaList.value = []; |
| | | mediaType.value = 'image'; |
| | | } |
| | | |
| | | // 表单关闭方法 |
| | | // 关闭弹窗 |
| | | const cancel = () => { |
| | | dialogVisitable.value = false; |
| | | showVideoDialog.value = false; |
| | | rowData.value = {}; |
| | | attachmentList.value = []; |
| | | }; |
| | | |
| | | defineExpose({ openDialog }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .upload-container { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | padding: 20px; |
| | | border: 1px solid #dcdfe6; |
| | | box-sizing: border-box; |
| | | .detail-container { |
| | | max-height: 60vh; |
| | | overflow-y: auto; |
| | | } |
| | | |
| | | .form-container { |
| | | flex: 1; |
| | | width: 100%; |
| | | .info-section { |
| | | margin-bottom: 20px; |
| | | |
| | | &:last-child { |
| | | margin-bottom: 0; |
| | | } |
| | | } |
| | | |
| | | .title { |
| | | .section-title { |
| | | font-size: 14px; |
| | | color: #165dff; |
| | | line-height: 20px; |
| | | font-weight: 600; |
| | | padding-left: 10px; |
| | | position: relative; |
| | | margin: 6px 0; |
| | | margin-bottom: 12px; |
| | | |
| | | &::before { |
| | | content: ""; |
| | |
| | | } |
| | | } |
| | | |
| | | .media-viewer-overlay { |
| | | position: fixed; |
| | | top: 0; |
| | | left: 0; |
| | | right: 0; |
| | | bottom: 0; |
| | | background-color: rgba(0, 0, 0, 0.8); |
| | | z-index: 9999; |
| | | .exception-content { |
| | | padding: 12px; |
| | | background-color: #fff2f0; |
| | | border: 1px solid #ffccc7; |
| | | border-radius: 4px; |
| | | color: #ff4d4f; |
| | | font-size: 14px; |
| | | line-height: 1.6; |
| | | } |
| | | |
| | | .attachment-list { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 12px; |
| | | } |
| | | |
| | | .attachment-item { |
| | | width: 100px; |
| | | text-align: center; |
| | | } |
| | | |
| | | .attachment-preview { |
| | | width: 100px; |
| | | height: 100px; |
| | | border: 1px solid #dcdfe6; |
| | | border-radius: 4px; |
| | | overflow: hidden; |
| | | cursor: pointer; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | background-color: #f5f7fa; |
| | | transition: all 0.3s; |
| | | |
| | | &:hover { |
| | | border-color: #409eff; |
| | | transform: scale(1.02); |
| | | } |
| | | |
| | | .media-viewer-content { |
| | | position: relative; |
| | | max-width: 90vw; |
| | | max-height: 90vh; |
| | | img { |
| | | width: 100%; |
| | | height: 100%; |
| | | object-fit: cover; |
| | | } |
| | | |
| | | video { |
| | | width: 100%; |
| | | height: 100%; |
| | | object-fit: cover; |
| | | } |
| | | |
| | | .file-icon { |
| | | font-size: 32px; |
| | | color: #909399; |
| | | } |
| | | } |
| | | |
| | | .attachment-info { |
| | | margin-top: 4px; |
| | | |
| | | .file-name { |
| | | font-size: 12px; |
| | | color: #606266; |
| | | display: block; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | white-space: nowrap; |
| | | } |
| | | } |
| | | |
| | | .empty-attachment { |
| | | padding: 30px; |
| | | text-align: center; |
| | | color: #909399; |
| | | background-color: #f5f7fa; |
| | | border-radius: 4px; |
| | | } |
| | | </style> |