| | |
| | | <text class="task-location">{{ item.inspectionLocation }}</text> |
| | | </view> |
| | | <view class="task-actions"> |
| | | <u-button type="primary" |
| | | <!-- <u-button type="primary" |
| | | size="small" |
| | | @click.stop="startScanForTask(item)" |
| | | :customStyle="{ |
| | |
| | | marginRight: '8px' |
| | | }"> |
| | | 扫码上传 |
| | | </u-button> --> |
| | | <u-button type="primary" |
| | | size="small" |
| | | @click.stop="startUploadForTask(item)" |
| | | :customStyle="{ |
| | | borderRadius: '15px', |
| | | height: '30px', |
| | | fontSize: '12px', |
| | | marginRight: '8px' |
| | | }"> |
| | | 图片上传 |
| | | </u-button> |
| | | <u-button type="success" |
| | | size="small" |
| | |
| | | <view v-for="(file, index) in getCurrentFiles()" |
| | | :key="index" |
| | | class="file-item"> |
| | | <view class="file-preview-container"> |
| | | <image v-if="file.type === 'image' || (file.type !== 'video' && !file.type)" |
| | | :src="file.url || file.tempFilePath || file.path || file.downloadUrl" |
| | | <view class="file-preview-container" |
| | | @click="previewUploadedMedia(file)"> |
| | | <image v-if="isImageFile(file)" |
| | | :src="getFileAccessUrl(file)" |
| | | class="file-preview" |
| | | mode="aspectFill" /> |
| | | <view v-else-if="file.type === 'video'" |
| | | <view v-else-if="isVideoFile(file)" |
| | | class="video-preview"> |
| | | <uni-icons type="videocam" |
| | | name="videocam" |
| | |
| | | </view> |
| | | </view> |
| | | <view class="file-info"> |
| | | <text class="file-name">{{ file.bucketFilename || file.name || (file.type === 'image' ? '图片' : '视频') |
| | | <text class="file-name">{{ file.bucketFilename || file.name || (isImageFile(file) ? '图片' : '视频') |
| | | }}</text> |
| | | <text class="file-size">{{ formatFileSize(file.size) }}</text> |
| | | </view> |
| | |
| | | @click="previewAttachment(file)"> |
| | | <view class="attachment-preview-container"> |
| | | <image v-if="file.type === 'image' || isImageFile(file)" |
| | | :src="file.url || file.downloadUrl" |
| | | :src="getFileAccessUrl(file)" |
| | | class="attachment-preview" |
| | | mode="aspectFill" /> |
| | | <view v-else |
| | |
| | | </view> |
| | | <view class="video-modal-body"> |
| | | <video v-if="currentVideoFile" |
| | | :src="currentVideoFile.url || currentVideoFile.downloadUrl" |
| | | :src="currentVideoFile._playUrl" |
| | | class="video-player" |
| | | controls |
| | | autoplay |
| | |
| | | } |
| | | }; |
| | | |
| | | // 图片上传(可选择图片上传或者是相机拍照) |
| | | const startUploadForTask = async (task, type) => { |
| | | // 直接打开上传弹窗 |
| | | openUploadDialog(task); |
| | | }; |
| | | |
| | | // 查看附件 |
| | | const viewAttachments = async task => { |
| | | try { |
| | |
| | | : allList.filter(f => f?.type === 12); |
| | | |
| | | const mapToViewFile = (file, viewType) => { |
| | | const u = normalizeFileUrl(file?.url || file?.downloadUrl || ""); |
| | | const u = getFileAccessUrl(file); |
| | | return { |
| | | ...file, |
| | | // 用于三标签页分组:0=生产前 1=生产中 2=生产后 |
| | | type: viewType, |
| | | viewType, |
| | | name: file?.name || file?.originalFilename || file?.bucketFilename, |
| | | bucketFilename: file?.bucketFilename || file?.name, |
| | | originalFilename: file?.originalFilename || file?.name, |
| | |
| | | |
| | | // 根据type获取对应分类的附件 |
| | | const getAttachmentsByType = typeValue => { |
| | | return attachmentList.value.filter(file => file.type === typeValue) || []; |
| | | return attachmentList.value.filter(file => file.viewType === typeValue) || []; |
| | | }; |
| | | // 获取type值 |
| | | const getTabType = () => { |
| | |
| | | return true; |
| | | } |
| | | |
| | | // 检查原有的type字段 |
| | | if (file.type === "image") return true; |
| | | // 检查原有的type字段(或保留的媒体类型) |
| | | if (file.type === "image" || file.mediaType === "image") return true; |
| | | |
| | | // 检查文件扩展名 |
| | | const name = file.bucketFilename || file.originalFilename || file.name || ""; |
| | |
| | | return ["jpg", "jpeg", "png", "gif", "webp"].includes(ext); |
| | | }; |
| | | |
| | | // 判断是否为视频文件 |
| | | const isVideoFile = file => { |
| | | if (!file) return false; |
| | | if (file.type === "video" || file.mediaType === "video") return true; |
| | | const name = file.bucketFilename || file.originalFilename || file.name || ""; |
| | | const ext = name.split(".").pop()?.toLowerCase(); |
| | | return ["mp4", "mov", "avi", "wmv", "mkv", "webm"].includes(ext); |
| | | }; |
| | | |
| | | // 文件访问基础域(后端要求前缀) |
| | | const filePreviewBase = config.fileUrl; |
| | | |
| | | // 将后端返回的文件地址规范成可访问URL |
| | | // 兼容场景: |
| | | // - 已经是 http/https:直接返回 |
| | | // - 以 / 开头:拼接 filePreviewBase |
| | | // - Windows 本地路径(如 D:\ruoyi\prod\uploads...\xx.jpg):尝试截取 prod 之后的相对路径并拼接 filePreviewBase |
| | | const normalizeFileUrl = rawUrl => { |
| | | try { |
| | | if (!rawUrl || typeof rawUrl !== "string") return ""; |
| | | const url = rawUrl.trim(); |
| | | if (!url) return ""; |
| | | if (/^https?:\/\//i.test(url)) return url; |
| | | if (url.startsWith("/")) return `${filePreviewBase}${url}`; |
| | | const normalizeFileUrl = (rawUrl = "") => { |
| | | let fileUrl = rawUrl || ""; |
| | | if (typeof fileUrl === "string") { |
| | | fileUrl = fileUrl.trim().replace(/^['"]|['"]$/g, ""); |
| | | } |
| | | const javaApi = filePreviewBase; |
| | | const localPrefixes = ["wxfile://", "file://", "content://", "blob:", "data:"]; |
| | | |
| | | // Windows path -> web path |
| | | if (/^[a-zA-Z]:\\/.test(url)) { |
| | | const normalized = url.replace(/\\/g, "/"); |
| | | const idx = normalized.indexOf("/prod/"); |
| | | if (idx >= 0) { |
| | | const relative = normalized.slice(idx + "/prod/".length); |
| | | return `${filePreviewBase}/${relative}`; |
| | | } |
| | | // 兜底:无法推断映射规则时,至少把反斜杠变成正斜杠 |
| | | return normalized; |
| | | if (localPrefixes.some(prefix => fileUrl.startsWith(prefix))) { |
| | | return fileUrl; |
| | | } |
| | | |
| | | if (fileUrl && fileUrl.indexOf("\\") > -1) { |
| | | const lowerPath = fileUrl.toLowerCase(); |
| | | const uploadPathIndex = lowerPath.indexOf("uploadpath"); |
| | | const prodIndex = lowerPath.indexOf("\\prod\\"); |
| | | |
| | | if (uploadPathIndex > -1) { |
| | | fileUrl = fileUrl.substring(uploadPathIndex).replace(/\\/g, "/"); |
| | | } else if (prodIndex > -1) { |
| | | fileUrl = fileUrl |
| | | .substring(prodIndex + "\\prod\\".length) |
| | | .replace(/\\/g, "/"); |
| | | } else { |
| | | fileUrl = fileUrl.replace(/\\/g, "/"); |
| | | } |
| | | } |
| | | // /javaWork/.../file/prod/xxx -> /profile/prod/xxx |
| | | const normalizedLower = String(fileUrl).toLowerCase(); |
| | | const fileProdIdx = normalizedLower.indexOf("/file/prod/"); |
| | | if (fileProdIdx > -1) { |
| | | fileUrl = `/profile/prod/${fileUrl.substring(fileProdIdx + "/file/prod/".length)}`; |
| | | } |
| | | // /javaWork/.../file/temp/xxx -> /profile/temp/xxx |
| | | const fileTempIdx = normalizedLower.indexOf("/file/temp/"); |
| | | if (fileTempIdx > -1) { |
| | | fileUrl = `/profile/temp/${fileUrl.substring(fileTempIdx + "/file/temp/".length)}`; |
| | | } |
| | | |
| | | // 其他相对路径:直接用 baseUrl 拼一下 |
| | | return `${filePreviewBase}/${url.replace(/^\//, "")}`; |
| | | } catch (e) { |
| | | return rawUrl || ""; |
| | | if (/^\/?uploadPath/i.test(fileUrl)) { |
| | | fileUrl = fileUrl.replace(/^\/?uploadPath/i, "/profile"); |
| | | } |
| | | |
| | | if (fileUrl && !fileUrl.startsWith("http")) { |
| | | if (!fileUrl.startsWith("/")) fileUrl = "/" + fileUrl; |
| | | fileUrl = javaApi + fileUrl; |
| | | } |
| | | |
| | | return fileUrl; |
| | | }; |
| | | |
| | | const getFileAccessUrl = (file = {}) => { |
| | | if (file?.link) { |
| | | if (String(file.link).startsWith("http")) return file.link; |
| | | return normalizeFileUrl(file.link); |
| | | } |
| | | const remoteUrl = normalizeFileUrl( |
| | | file?.url || |
| | | file?.downloadUrl || |
| | | file?.tempPath || |
| | | "" |
| | | ); |
| | | if (remoteUrl) return remoteUrl; |
| | | // 兜底本地路径(上传当次预览) |
| | | if (file?._localPreviewUrl) return file._localPreviewUrl; |
| | | return normalizeFileUrl(file?.tempFilePath || file?.path || ""); |
| | | }; |
| | | |
| | | // 上传弹窗内的媒体预览(图片/视频) |
| | | const previewUploadedMedia = file => { |
| | | if (!file) return; |
| | | if (isImageFile(file)) { |
| | | const imageUrls = getCurrentFiles() |
| | | .filter(f => isImageFile(f)) |
| | | .map(f => getFileAccessUrl(f)) |
| | | .filter(Boolean); |
| | | const current = getFileAccessUrl(file); |
| | | if (!imageUrls.length || !current) return; |
| | | uni.previewImage({ |
| | | urls: imageUrls, |
| | | current, |
| | | }); |
| | | return; |
| | | } |
| | | if (isVideoFile(file)) { |
| | | playVideoFile(file); |
| | | } |
| | | }; |
| | | |
| | |
| | | // 预览图片 |
| | | const imageUrls = getCurrentViewAttachments() |
| | | .filter(f => isImageFile(f)) |
| | | .map(f => f.url || f.downloadUrl); |
| | | .map(f => getFileAccessUrl(f)); |
| | | |
| | | uni.previewImage({ |
| | | urls: imageUrls, |
| | | current: file.url || file.downloadUrl, |
| | | current: getFileAccessUrl(file), |
| | | }); |
| | | } else { |
| | | // 预览视频 - 显示视频播放弹窗 |
| | | showVideoPreview(file); |
| | | // 预览视频 |
| | | playVideoFile(file); |
| | | } |
| | | }; |
| | | |
| | | const safeEncodeUrl = url => { |
| | | if (!url) return ""; |
| | | if (!/^https?:\/\//i.test(url)) return url; |
| | | try { |
| | | return encodeURI(url); |
| | | } catch (e) { |
| | | return url; |
| | | } |
| | | }; |
| | | |
| | | const getPlayableVideoUrl = file => { |
| | | return safeEncodeUrl(getFileAccessUrl(file)); |
| | | }; |
| | | |
| | | const getFallbackLocalVideoUrl = file => { |
| | | const local = file?._localPreviewUrl || file?.tempFilePath || file?.path || ""; |
| | | return safeEncodeUrl(local); |
| | | }; |
| | | |
| | | const playVideoFile = file => { |
| | | const remoteSrc = getPlayableVideoUrl(file); |
| | | const localSrc = getFallbackLocalVideoUrl(file); |
| | | // 弹窗播放器优先远程地址,失败再自动回退本地地址 |
| | | const src = remoteSrc || localSrc; |
| | | if (!src) { |
| | | uni.showToast({ |
| | | title: "视频地址无效", |
| | | icon: "none", |
| | | }); |
| | | return; |
| | | } |
| | | |
| | | // 统一使用页面内 video 弹窗播放 |
| | | showVideoPreview({ |
| | | ...file, |
| | | _playUrl: src, |
| | | _playUrlLocal: localSrc, |
| | | _playUrlRemote: remoteSrc, |
| | | }); |
| | | }; |
| | | |
| | | // 显示视频预览 |
| | |
| | | }; |
| | | |
| | | // 视频播放错误处理 |
| | | const handleVideoError = error => { |
| | | const handleVideoError = () => { |
| | | const localUrl = currentVideoFile.value?._playUrlLocal; |
| | | const remoteUrl = currentVideoFile.value?._playUrlRemote; |
| | | const currentUrl = currentVideoFile.value?._playUrl; |
| | | if (remoteUrl && currentUrl !== remoteUrl) { |
| | | currentVideoFile.value = { |
| | | ...currentVideoFile.value, |
| | | _playUrl: remoteUrl, |
| | | }; |
| | | uni.showToast({ |
| | | title: "已切换远程视频重试播放", |
| | | icon: "none", |
| | | }); |
| | | return; |
| | | } |
| | | if (localUrl && currentUrl !== localUrl) { |
| | | currentVideoFile.value = { |
| | | ...currentVideoFile.value, |
| | | _playUrl: localUrl, |
| | | }; |
| | | uni.showToast({ |
| | | title: "已切换本地视频重试播放", |
| | | icon: "none", |
| | | }); |
| | | return; |
| | | } |
| | | uni.showToast({ |
| | | title: "视频播放失败", |
| | | icon: "error", |
| | |
| | | url: |
| | | uploadedFile.url || |
| | | uploadedFile.downloadUrl || |
| | | uploadedFile.tempPath || |
| | | file.tempFilePath || |
| | | file.path, |
| | | tempPath: uploadedFile.tempPath || file.tempPath || "", |
| | | _localPreviewUrl: file.tempFilePath || file.path || "", |
| | | bucketFilename: |
| | | uploadedFile.bucketFilename || uploadedFile.originalFilename || file.name, |
| | | uploadedFile.bucketFilename || |
| | | uploadedFile.originalFilename || |
| | | uploadedFile.originalName || |
| | | file.name, |
| | | downloadUrl: uploadedFile.downloadUrl || uploadedFile.url, |
| | | size: uploadedFile.size || uploadedFile.byteSize || file.size, |
| | | createTime: uploadedFile.createTime || new Date().getTime(), |
| | | mediaType: file.type || uploadedFile.mediaType, |
| | | type: typeValue, // 添加类型字段:0=生产前, 1=生产中, 2=生产后 |
| | | }; |
| | | |
| | |
| | | |
| | | .video-player { |
| | | width: 100%; |
| | | height: auto; |
| | | height: 56vh; |
| | | min-height: 260px; |
| | | max-height: 60vh; |
| | | display: block; |
| | | object-fit: contain; |
| | | } |
| | | </style> |