| src/pages/inspectionUpload/attachment.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/pages/inspectionUpload/components/formDia.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/pages/inspectionUpload/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/pages/inspectionUpload/upload.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
src/pages/inspectionUpload/attachment.vue
@@ -1,53 +1,28 @@ <template> <view class="attachment-page"> <!-- 页面头部 --> <PageHeader :title="`查看附件 - ${taskInfo?.taskName || ''}`" @back="goBack" /> <PageHeader :title="`查看附件 - ${taskInfo?.taskName || ''}`" @back="goBack" /> <!-- 页面内容 --> <view class="attachment-content"> <!-- 分类标签页 --> <view class="attachment-tabs"> <view class="tab-item" :class="{ active: currentViewType === 'before' }" @click="switchViewType('before')" > 生产前 ({{ getAttachmentsByType(0).length }}) </view> <view class="tab-item" :class="{ active: currentViewType === 'after' }" @click="switchViewType('after')" > 生产中 ({{ getAttachmentsByType(1).length }}) </view> <view class="tab-item" :class="{ active: currentViewType === 'issue' }" @click="switchViewType('issue')" > 生产后 ({{ getAttachmentsByType(2).length }}) </view> </view> <!-- 当前分类的附件列表 --> <view class="attachment-list-container"> <view v-if="getCurrentViewAttachments().length > 0" class="attachment-list"> <view v-for="(file, index) in getCurrentViewAttachments()" :key="index" class="attachment-item" @click="previewAttachment(file)" > <view v-if="attachmentList.length > 0" class="attachment-list"> <view v-for="(file, index) in attachmentList" :key="index" class="attachment-item" @click="previewAttachment(file)"> <view class="attachment-preview-container"> <image v-if="isImageFile(file)" :src="file.url || file.downloadUrl" class="attachment-preview" mode="aspectFill" /> <view v-else class="attachment-video-preview"> <u-icon name="video" size="40" color="#409eff"></u-icon> <image v-if="isImageFile(file)" :src="file.url || file.downloadUrl" class="attachment-preview" mode="aspectFill" /> <view v-else class="attachment-video-preview"> <u-icon name="video" size="40" color="#409eff"></u-icon> <text class="video-text">视频</text> </view> </view> @@ -57,31 +32,37 @@ </view> </view> </view> <view v-else class="attachment-empty"> <u-icon name="folder-open" size="60" color="#ccc"></u-icon> <view v-else class="attachment-empty"> <u-icon name="folder-open" size="60" color="#ccc"></u-icon> <text class="empty-text">该分类暂无附件</text> </view> </view> </view> <!-- 视频预览弹窗 --> <view v-if="showVideoDialog" class="video-modal-overlay" @click="closeVideoPreview"> <view class="video-modal-container" @click.stop> <view v-if="showVideoDialog" class="video-modal-overlay" @click="closeVideoPreview"> <view class="video-modal-container" @click.stop> <view class="video-modal-header"> <text class="video-modal-title">{{ currentVideoFile?.originalFilename || '视频预览' }}</text> <view class="close-btn-video" @click="closeVideoPreview"> <u-icon name="close" size="20" color="#fff"></u-icon> <view class="close-btn-video" @click="closeVideoPreview"> <u-icon name="close" size="20" color="#fff"></u-icon> </view> </view> <view class="video-modal-body"> <video v-if="currentVideoFile" :src="currentVideoFile.url || currentVideoFile.downloadUrl" class="video-player" controls autoplay @error="handleVideoError" ></video> <video v-if="currentVideoFile" :src="currentVideoFile.url || currentVideoFile.downloadUrl" class="video-player" controls autoplay @error="handleVideoError"></video> </view> </view> </view> @@ -89,372 +70,351 @@ </template> <script setup> import { ref } from 'vue'; import { onLoad } from '@dcloudio/uni-app'; import PageHeader from '@/components/PageHeader.vue'; import config from '@/config'; import { ref } from "vue"; import { onLoad } from "@dcloudio/uni-app"; import PageHeader from "@/components/PageHeader.vue"; import config from "@/config"; // 任务信息 const taskInfo = ref(null); // 任务信息 const taskInfo = ref(null); // 附件列表 const attachmentList = ref([]); // 附件列表 const attachmentList = ref([]); // 当前查看类型 const currentViewType = ref('before'); // 'before', 'after', 'issue' // 视频预览相关状态 const showVideoDialog = ref(false); const currentVideoFile = ref(null); // 视频预览相关状态 const showVideoDialog = ref(false); const currentVideoFile = ref(null); // 文件访问基础域 const filePreviewBase = config.fileUrl; // 文件访问基础域 const filePreviewBase = config.fileUrl; // 页面加载 onLoad((options) => { if (options.taskInfo) { try { taskInfo.value = JSON.parse(decodeURIComponent(options.taskInfo)); loadAttachments(); } catch (e) { console.error('解析任务信息失败:', e); uni.showToast({ title: '加载失败', icon: 'error' }); // 页面加载 onLoad(options => { if (options.taskInfo) { try { taskInfo.value = JSON.parse(decodeURIComponent(options.taskInfo)); loadAttachments(); } catch (e) { console.error("解析任务信息失败:", e); uni.showToast({ title: "加载失败", icon: "error", }); } } } }); }); // 加载附件数据 const loadAttachments = () => { const task = taskInfo.value; if (!task) return; // 加载附件数据 const loadAttachments = () => { const task = taskInfo.value; if (!task) return; attachmentList.value = []; attachmentList.value = []; // 后端反显字段 const allList = Array.isArray(task?.commonFileList) ? task.commonFileList : []; const beforeList = Array.isArray(task?.commonFileListBefore) ? task.commonFileListBefore : allList.filter((f) => f?.type === 10); const afterList = Array.isArray(task?.commonFileListAfter) ? task.commonFileListAfter : allList.filter((f) => f?.type === 11); const issueList = Array.isArray(task?.commonFileListIssue) ? task.commonFileListIssue : allList.filter((f) => f?.type === 12); // 获取附件列表,优先从 commonFileListBeforeVO 获取 let rawList = []; if (Array.isArray(task.commonFileListBeforeVO)) { rawList = task.commonFileListBeforeVO; } else if (Array.isArray(task.commonFileListBefore)) { rawList = task.commonFileListBefore; } else if (Array.isArray(task.commonFileList)) { // 降级:从通用列表过滤 type 为 10 的 rawList = task.commonFileList.filter(f => f?.type === 10); } const mapToViewFile = (file, viewType) => { const u = normalizeFileUrl(file?.url || file?.downloadUrl || ''); return { ...file, type: viewType, name: file?.name || file?.originalFilename || file?.bucketFilename, bucketFilename: file?.bucketFilename || file?.name, originalFilename: file?.originalFilename || file?.name, url: u, downloadUrl: u, size: file?.size || file?.byteSize, const mapToViewFile = file => { // 优先使用 previewURL 或 url const rawUrl = file?.previewURL || file?.url || file?.downloadUrl || file?.downloadURL || ""; const u = normalizeFileUrl(rawUrl); return { ...file, name: file?.name || file?.originalFilename || file?.bucketFilename || "附件", bucketFilename: file?.bucketFilename || file?.name || file?.originalFilename, originalFilename: file?.originalFilename || file?.name, url: u, downloadUrl: u, size: file?.size || file?.byteSize || 0, }; }; attachmentList.value = rawList.map(f => mapToViewFile(f)); }; attachmentList.value.push(...beforeList.map((f) => mapToViewFile(f, 0))); attachmentList.value.push(...afterList.map((f) => mapToViewFile(f, 1))); attachmentList.value.push(...issueList.map((f) => mapToViewFile(f, 2))); }; // 将后端返回的文件地址规范成可访问URL 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}`; // 将后端返回的文件地址规范成可访问URL 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}`; // 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}`; // 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; } return normalized; return `${filePreviewBase}/${url.replace(/^\//, "")}`; } catch (e) { return rawUrl || ""; } }; return `${filePreviewBase}/${url.replace(/^\//, '')}`; } catch (e) { return rawUrl || ''; } }; // 返回上一页 const goBack = () => { uni.navigateBack(); }; // 返回上一页 const goBack = () => { uni.navigateBack(); }; // 判断是否为图片文件 const isImageFile = file => { if (file.contentType && file.contentType.startsWith("image/")) { return true; } if (file.type === "image") return true; // 切换查看类型 const switchViewType = (type) => { currentViewType.value = type; }; const name = file.bucketFilename || file.originalFilename || file.name || ""; const ext = name.split(".").pop()?.toLowerCase(); return ["jpg", "jpeg", "png", "gif", "webp"].includes(ext); }; // 根据type获取对应分类的附件 const getAttachmentsByType = (typeValue) => { return attachmentList.value.filter((file) => file.type === typeValue) || []; }; // 预览附件 const previewAttachment = file => { if (isImageFile(file)) { const imageUrls = attachmentList.value .filter(f => isImageFile(f)) .map(f => f.url || f.downloadUrl); // 获取当前查看类型的附件 const getCurrentViewAttachments = () => { switch (currentViewType.value) { case 'before': return getAttachmentsByType(0); case 'after': return getAttachmentsByType(1); case 'issue': return getAttachmentsByType(2); default: return []; } }; uni.previewImage({ urls: imageUrls, current: file.url || file.downloadUrl, }); } else { showVideoPreview(file); } }; // 判断是否为图片文件 const isImageFile = (file) => { if (file.contentType && file.contentType.startsWith('image/')) { return true; } if (file.type === 'image') return true; // 显示视频预览 const showVideoPreview = file => { currentVideoFile.value = file; showVideoDialog.value = true; }; const name = file.bucketFilename || file.originalFilename || file.name || ''; const ext = name.split('.').pop()?.toLowerCase(); return ['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(ext); }; // 关闭视频预览 const closeVideoPreview = () => { showVideoDialog.value = false; currentVideoFile.value = null; }; // 预览附件 const previewAttachment = (file) => { if (isImageFile(file)) { const imageUrls = getCurrentViewAttachments() .filter((f) => isImageFile(f)) .map((f) => f.url || f.downloadUrl); uni.previewImage({ urls: imageUrls, current: file.url || file.downloadUrl, // 视频播放错误处理 const handleVideoError = () => { uni.showToast({ title: "视频播放失败", icon: "error", }); } else { showVideoPreview(file); } }; }; // 显示视频预览 const showVideoPreview = (file) => { currentVideoFile.value = file; showVideoDialog.value = true; }; // 关闭视频预览 const closeVideoPreview = () => { showVideoDialog.value = false; currentVideoFile.value = null; }; // 视频播放错误处理 const handleVideoError = () => { uni.showToast({ title: '视频播放失败', icon: 'error', }); }; // 格式化文件大小 const formatFileSize = (size) => { if (!size) return ''; if (size < 1024) return size + 'B'; if (size < 1024 * 1024) return (size / 1024).toFixed(1) + 'KB'; return (size / (1024 * 1024)).toFixed(1) + 'MB'; }; // 格式化文件大小 const formatFileSize = size => { if (!size) return ""; if (size < 1024) return size + "B"; if (size < 1024 * 1024) return (size / 1024).toFixed(1) + "KB"; return (size / (1024 * 1024)).toFixed(1) + "MB"; }; </script> <style scoped> .attachment-page { min-height: 100vh; background-color: #f5f5f5; } .attachment-page { min-height: 100vh; background-color: #f5f5f5; } .attachment-content { padding: 15px; } .attachment-content { padding: 15px; } /* 标签页样式 */ .attachment-tabs { display: flex; background: #fff; border-radius: 12px; margin-bottom: 15px; padding: 4px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); } /* 标签页样式 */ .attachment-tabs { display: flex; background: #fff; border-radius: 12px; margin-bottom: 15px; padding: 4px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); } .tab-item { flex: 1; text-align: center; padding: 12px 8px; font-size: 14px; color: #666; border-radius: 8px; transition: all 0.3s ease; } .tab-item { flex: 1; text-align: center; padding: 12px 8px; font-size: 14px; color: #666; border-radius: 8px; transition: all 0.3s ease; } .tab-item.active { background: #409eff; color: #fff; font-weight: 500; } .tab-item.active { background: #409eff; color: #fff; font-weight: 500; } /* 附件列表样式 */ .attachment-list-container { background: #fff; border-radius: 12px; padding: 15px; min-height: 400px; } /* 附件列表样式 */ .attachment-list-container { background: #fff; border-radius: 12px; padding: 15px; min-height: 400px; } .attachment-list { display: flex; flex-wrap: wrap; gap: 15px; } .attachment-list { display: flex; flex-wrap: wrap; gap: 15px; } .attachment-item { width: calc(33.33% - 10px); background: #f8f9fa; border-radius: 12px; overflow: hidden; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); transition: all 0.3s ease; } .attachment-item { width: calc(33.33% - 10px); background: #f8f9fa; border-radius: 12px; overflow: hidden; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); transition: all 0.3s ease; } .attachment-item:active { transform: scale(0.98); } .attachment-item:active { transform: scale(0.98); } .attachment-preview-container { width: 100%; height: 120px; background: #e9ecef; display: flex; align-items: center; justify-content: center; } .attachment-preview-container { width: 100%; height: 120px; background: #e9ecef; display: flex; align-items: center; justify-content: center; } .attachment-preview { width: 100%; height: 100%; object-fit: cover; } .attachment-preview { width: 100%; height: 100%; object-fit: cover; } .attachment-video-preview { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; } .attachment-video-preview { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; } .video-text { font-size: 12px; color: #666; } .video-text { font-size: 12px; color: #666; } .attachment-info { padding: 10px; } .attachment-info { padding: 10px; } .attachment-name { font-size: 12px; color: #333; display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-bottom: 4px; } .attachment-name { font-size: 12px; color: #333; display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-bottom: 4px; } .attachment-size { font-size: 10px; color: #999; } .attachment-size { font-size: 10px; color: #999; } /* 空状态样式 */ .attachment-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 80px 20px; color: #999; } /* 空状态样式 */ .attachment-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 80px 20px; color: #999; } .empty-text { margin-top: 15px; font-size: 14px; } .empty-text { margin-top: 15px; font-size: 14px; } /* 视频弹窗样式 */ .video-modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.9); z-index: 10000; display: flex; align-items: center; justify-content: center; padding: 20px; } /* 视频弹窗样式 */ .video-modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.9); z-index: 10000; display: flex; align-items: center; justify-content: center; padding: 20px; } .video-modal-container { width: 100%; max-width: 800px; background: #000; border-radius: 12px; overflow: hidden; } .video-modal-container { width: 100%; max-width: 800px; background: #000; border-radius: 12px; overflow: hidden; } .video-modal-header { display: flex; justify-content: space-between; align-items: center; padding: 15px 20px; background: #1a1a1a; } .video-modal-header { display: flex; justify-content: space-between; align-items: center; padding: 15px 20px; background: #1a1a1a; } .video-modal-title { font-size: 16px; color: #fff; font-weight: 500; } .video-modal-title { font-size: 16px; color: #fff; font-weight: 500; } .close-btn-video { width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; background: rgba(255, 255, 255, 0.1); border-radius: 50%; } .close-btn-video { width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; background: rgba(255, 255, 255, 0.1); border-radius: 50%; } .video-modal-body { padding: 20px; } .video-modal-body { padding: 20px; } .video-player { width: 100%; height: 400px; border-radius: 8px; } .video-player { width: 100%; height: 400px; border-radius: 8px; } </style> src/pages/inspectionUpload/components/formDia.vue
@@ -1,460 +1,398 @@ <template> <u-popup v-model="dialogVisitable" mode="center" :round="10" :closeable="true" @close="cancel" > <u-popup v-model="dialogVisitable" mode="center" :round="10" :closeable="true" @close="cancel"> <view class="popup-content"> <view class="popup-header"> <text class="popup-title">巡检记录上传</text> </view> <view class="upload-container"> <!-- 异常状态选择 --> <view class="form-container"> <view class="title">巡检状态</view> <view class="exception-section"> <view class="exception-options"> <view class="exception-option" :class="{ active: hasException === false }" @click="setExceptionStatus(false)" > <u-icon name="checkmark-circle" size="20" color="#52c41a"></u-icon> <view class="exception-option" :class="{ active: hasException === false }" @click="setExceptionStatus(false)"> <u-icon name="checkmark-circle" size="20" color="#52c41a"></u-icon> <text class="option-text">正常</text> </view> <view class="exception-option" :class="{ active: hasException === true }" @click="setExceptionStatus(true)" > <u-icon name="close-circle" size="20" color="#ff4d4f"></u-icon> <view class="exception-option" :class="{ active: hasException === true }" @click="setExceptionStatus(true)"> <u-icon name="close-circle" size="20" color="#ff4d4f"></u-icon> <text class="option-text">存在异常</text> </view> </view> </view> </view> <!-- 异常描述(仅在异常时显示) --> <view class="form-container" v-if="hasException === true"> <view class="form-container" v-if="hasException === true"> <view class="title">异常描述</view> <u-input v-model="exceptionDescription" type="textarea" :maxlength="500" placeholder="请描述异常情况..." :customStyle="{ padding: '10px', backgroundColor: '#f5f5f5' }" /> <u-input v-model="exceptionDescription" type="textarea" :maxlength="500" placeholder="请描述异常情况..." :customStyle="{ padding: '10px', backgroundColor: '#f5f5f5' }" /> </view> <!-- 上传区域(仅在异常时显示) --> <template v-if="hasException === true"> <view class="form-container"> <view class="title">生产前</view> <u-upload :fileList="beforeModelValue" @afterRead="afterRead" @delete="deleteFile" name="before" multiple :maxCount="10" :maxSize="5 * 1024 * 1024" accept="image/*" :previewFullImage="true" ></u-upload> </view> <view class="form-container"> <view class="title">生产后</view> <u-upload :fileList="afterModelValue" @afterRead="afterRead" @delete="deleteFile" name="after" multiple :maxCount="10" :maxSize="5 * 1024 * 1024" accept="image/*" :previewFullImage="true" ></u-upload> </view> <view class="form-container"> <view class="title">生产问题</view> <u-upload :fileList="issueModelValue" @afterRead="afterRead" @delete="deleteFile" name="issue" multiple :maxCount="10" :maxSize="5 * 1024 * 1024" accept="image/*" :previewFullImage="true" ></u-upload> <view class="title">巡检照片</view> <u-upload :fileList="beforeModelValue" @afterRead="afterRead" @delete="deleteFile" name="before" multiple :maxCount="10" :maxSize="5 * 1024 * 1024" accept="image/*" :previewFullImage="true"></u-upload> </view> </template> <!-- 正常状态提示 --> <view class="form-container normal-tip" v-if="hasException === false"> <u-icon name="info-circle" size="40" color="#52c41a"></u-icon> <view class="form-container normal-tip" v-if="hasException === false"> <u-icon name="info-circle" size="40" color="#52c41a"></u-icon> <text class="tip-text">设备运行正常,无需上传照片</text> </view> </view> <view class="popup-footer"> <u-button @click="cancel" :customStyle="{ marginRight: '10px' }">取消</u-button> <u-button type="primary" @click="submitForm">保存</u-button> <u-button @click="cancel" :customStyle="{ marginRight: '10px' }">取消</u-button> <u-button type="primary" @click="submitForm">保存</u-button> </view> </view> </u-popup> </template> <script setup> import { ref, computed } from 'vue' import { submitInspectionRecord } from '@/api/equipmentManagement/inspection.js' import { getToken } from '@/utils/auth' import config from '@/config' import { ref, computed } from "vue"; import { submitInspectionRecord } from "@/api/equipmentManagement/inspection.js"; import { getToken } from "@/utils/auth"; import config from "@/config"; const emit = defineEmits(['closeDia']) const emit = defineEmits(["closeDia"]); const dialogVisitable = ref(false) const beforeModelValue = ref([]) const afterModelValue = ref([]) const issueModelValue = ref([]) const infoData = ref(null) const dialogVisitable = ref(false); const beforeModelValue = ref([]); const infoData = ref(null); // 异常状态:null=未选择, false=正常, true=异常 const hasException = ref(null) // 异常描述 const exceptionDescription = ref('') // 异常状态:null=未选择, false=正常, true=异常 const hasException = ref(null); // 异常描述 const exceptionDescription = ref(""); // 计算上传URL const uploadFileUrl = computed(() => { let baseUrl = ''; if (process.env.VUE_APP_BASE_API) { baseUrl = process.env.VUE_APP_BASE_API; } else { baseUrl = config.baseUrl; } return baseUrl + '/file/upload'; }) // 计算上传URL const uploadFileUrl = computed(() => { let baseUrl = ""; const uploadSingleFile = async (fileItem, typeValue) => { const token = getToken() if (!token) throw new Error('用户未登录') // H5: u-upload 可能给原生 File(fileItem.file) const rawFile = fileItem?.file if (rawFile) { const formData = new FormData() formData.append('file', rawFile, rawFile.name || 'image.jpg') formData.append('type', String(typeValue)) const res = await fetch(uploadFileUrl.value, { method: 'POST', headers: { Authorization: 'Bearer ' + token }, body: formData }) const data = await res.json() if (data?.code !== 200) throw new Error(data?.msg || '上传失败') return { url: data?.data?.url, name: rawFile.name || 'image.jpg', status: 'success' if (process.env.VUE_APP_BASE_API) { baseUrl = process.env.VUE_APP_BASE_API; } else { baseUrl = config.baseUrl; } } // 非 H5 / 兼容:走 uni.uploadFile return await new Promise((resolve, reject) => { uni.uploadFile({ url: uploadFileUrl.value, filePath: fileItem.url, name: 'file', header: { 'Authorization': `Bearer ${token}` }, formData: { type: typeValue }, success: (res) => { try { const data = JSON.parse(res.data) if (data.code === 200) { resolve({ url: data.data.url, name: fileItem.name, status: 'success' }) } else { reject(new Error(data.msg || '上传失败')) return baseUrl + "/file/upload"; }); const uploadSingleFile = async (fileItem, typeValue) => { const token = getToken(); if (!token) throw new Error("用户未登录"); // H5: u-upload 可能给原生 File(fileItem.file) const rawFile = fileItem?.file; if (rawFile) { const formData = new FormData(); formData.append("file", rawFile, rawFile.name || "image.jpg"); formData.append("type", String(typeValue)); const res = await fetch(uploadFileUrl.value, { method: "POST", headers: { Authorization: "Bearer " + token }, body: formData, }); const data = await res.json(); if (data?.code !== 200) throw new Error(data?.msg || "上传失败"); return { url: data?.data?.url, name: rawFile.name || "image.jpg", status: "success", }; } // 非 H5 / 兼容:走 uni.uploadFile return await new Promise((resolve, reject) => { uni.uploadFile({ url: uploadFileUrl.value, filePath: fileItem.url, name: "file", header: { Authorization: `Bearer ${token}`, }, formData: { type: typeValue, }, success: res => { try { const data = JSON.parse(res.data); if (data.code === 200) { resolve({ url: data.data.url, name: fileItem.name, status: "success", }); } else { reject(new Error(data.msg || "上传失败")); } } catch (e) { reject(e); } } catch (e) { reject(e) }, fail: err => reject(err), }); }); }; // 文件上传处理 const afterRead = event => { const { file } = event; // 仅保留生产前(typeValue=10) let typeValue = 10; const files = Array.isArray(file) ? file : [file]; Promise.resolve() .then(async () => { for (const f of files) { const uploaded = await uploadSingleFile(f, typeValue); beforeModelValue.value.push(uploaded); } }, fail: (err) => reject(err) }) }) } // 文件上传处理 const afterRead = (event) => { const { name, file } = event // 根据上传类型设置不同的type值 let typeValue = 10 // 默认值 if (name === 'before') { typeValue = 10 // 生产前 } else if (name === 'after') { typeValue = 11 // 生产中 } else if (name === 'issue') { typeValue = 12 // 生产后 } const files = Array.isArray(file) ? file : [file] Promise.resolve().then(async () => { for (const f of files) { const uploaded = await uploadSingleFile(f, typeValue) if (name === 'before') { beforeModelValue.value.push(uploaded) } else if (name === 'after') { afterModelValue.value.push(uploaded) } else if (name === 'issue') { issueModelValue.value.push(uploaded) } } uni.showToast({ title: '上传成功', icon: 'success' }) }).catch((err) => { console.error('上传失败:', err) uni.showToast({ title: '上传失败', icon: 'error' }) }) } // 删除文件 const deleteFile = (event) => { const { name, index } = event if (name === 'before') { beforeModelValue.value.splice(index, 1) } else if (name === 'after') { afterModelValue.value.splice(index, 1) } else if (name === 'issue') { issueModelValue.value.splice(index, 1) } } // 设置异常状态 const setExceptionStatus = (status) => { hasException.value = status } // 提交表单 const submitForm = async () => { try { // 检查是否选择了巡检状态 if (hasException.value === null) { uni.showToast({ title: '请选择巡检状态', icon: 'none' uni.showToast({ title: "上传成功", icon: "success" }); }) return } .catch(err => { console.error("上传失败:", err); uni.showToast({ title: "上传失败", icon: "error" }); }); }; // 如果是异常状态,检查是否有上传文件 if (hasException.value === true) { const totalFiles = beforeModelValue.value.length + afterModelValue.value.length + issueModelValue.value.length if (totalFiles === 0) { // 删除文件 const deleteFile = event => { const { index } = event; beforeModelValue.value.splice(index, 1); }; // 设置异常状态 const setExceptionStatus = status => { hasException.value = status; }; // 提交表单 const submitForm = async () => { try { // 检查是否选择了巡检状态 if (hasException.value === null) { uni.showToast({ title: '请上传异常照片', icon: 'none' }) return title: "请选择巡检状态", icon: "none", }); return; } // 检查是否填写了异常描述 if (!exceptionDescription.value.trim()) { uni.showToast({ title: '请填写异常描述', icon: 'none' }) return // 如果是异常状态,检查是否有上传文件 if (hasException.value === true) { if (beforeModelValue.value.length === 0) { uni.showToast({ title: "请上传异常照片", icon: "none", }); return; } // 检查是否填写了异常描述 if (!exceptionDescription.value.trim()) { uni.showToast({ title: "请填写异常描述", icon: "none", }); return; } } } let arr = [] if (beforeModelValue.value.length > 0) { arr.push(...beforeModelValue.value.map(item => ({ ...item, statusType: 0 }))) } if (afterModelValue.value.length > 0) { arr.push(...afterModelValue.value.map(item => ({ ...item, statusType: 1 }))) } if (issueModelValue.value.length > 0) { arr.push(...issueModelValue.value.map(item => ({ ...item, statusType: 2 }))) } // 提交数据 infoData.value.storageBlobDTO = arr infoData.value.hasException = hasException.value infoData.value.exceptionDescription = exceptionDescription.value await submitInspectionRecord({ ...infoData.value }) uni.showToast({ title: '提交成功', icon: 'success' }) cancel() } catch (error) { console.error('提交失败:', error) uni.showToast({ title: '提交失败', icon: 'error' }) } } let arr = []; if (beforeModelValue.value.length > 0) { arr.push( ...beforeModelValue.value.map(item => ({ ...item, statusType: 0 })) ); } // 打开弹框 const openDialog = async (row) => { infoData.value = row dialogVisitable.value = true // 清空之前的数据 beforeModelValue.value = [] afterModelValue.value = [] issueModelValue.value = [] hasException.value = null exceptionDescription.value = '' } // 提交数据 infoData.value.storageBlobDTO = arr; infoData.value.hasException = hasException.value; infoData.value.exceptionDescription = exceptionDescription.value; await submitInspectionRecord({ ...infoData.value }); // 关闭弹框 const cancel = () => { dialogVisitable.value = false emit('closeDia') } uni.showToast({ title: "提交成功", icon: "success", }); defineExpose({ openDialog }) cancel(); } catch (error) { console.error("提交失败:", error); uni.showToast({ title: "提交失败", icon: "error", }); } }; // 打开弹框 const openDialog = async row => { infoData.value = row; dialogVisitable.value = true; // 清空之前的数据 beforeModelValue.value = []; hasException.value = null; exceptionDescription.value = ""; }; // 关闭弹框 const cancel = () => { dialogVisitable.value = false; emit("closeDia"); }; defineExpose({ openDialog }); </script> <style scoped lang="scss"> .popup-content { width: 90vw; max-width: 400px; background-color: #fff; border-radius: 10px; overflow: hidden; } .popup-header { padding: 20px 20px 10px; text-align: center; border-bottom: 1px solid #f0f0f0; } .popup-title { font-size: 18px; font-weight: 600; color: #333; } .upload-container { padding: 20px; max-height: 60vh; overflow-y: auto; } .form-container { margin-bottom: 20px; &:last-child { margin-bottom: 0; .popup-content { width: 90vw; max-width: 400px; background-color: #fff; border-radius: 10px; overflow: hidden; } } .title { font-size: 14px; color: #1890ff; line-height: 20px; font-weight: 600; padding-left: 10px; position: relative; margin: 6px 0 10px; &::before { content: ""; position: absolute; left: 0; top: 3px; width: 4px; height: 14px; background-color: #1890ff; .popup-header { padding: 20px 20px 10px; text-align: center; border-bottom: 1px solid #f0f0f0; } } .popup-footer { display: flex; justify-content: center; padding: 15px 20px; border-top: 1px solid #f0f0f0; background-color: #fafafa; } // 异常状态选择样式 .exception-section { padding: 10px 0; } .exception-options { display: flex; gap: 15px; } .exception-option { flex: 1; display: flex; align-items: center; justify-content: center; gap: 8px; padding: 15px 20px; border: 2px solid #e0e0e0; border-radius: 8px; cursor: pointer; transition: all 0.3s; background-color: #fff; &.active { border-color: #1890ff; background-color: #e6f7ff; .popup-title { font-size: 18px; font-weight: 600; color: #333; } &:active { opacity: 0.8; .upload-container { padding: 20px; max-height: 60vh; overflow-y: auto; } } .option-text { font-size: 14px; color: #333; font-weight: 500; } .form-container { margin-bottom: 20px; // 正常状态提示样式 .normal-tip { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 40px 20px; background-color: #f6ffed; border: 1px dashed #b7eb8f; border-radius: 8px; .tip-text { margin-top: 15px; &:last-child { margin-bottom: 0; } } .title { font-size: 14px; color: #52c41a; color: #1890ff; line-height: 20px; font-weight: 600; padding-left: 10px; position: relative; margin: 6px 0 10px; &::before { content: ""; position: absolute; left: 0; top: 3px; width: 4px; height: 14px; background-color: #1890ff; } } } .popup-footer { display: flex; justify-content: center; padding: 15px 20px; border-top: 1px solid #f0f0f0; background-color: #fafafa; } // 异常状态选择样式 .exception-section { padding: 10px 0; } .exception-options { display: flex; gap: 15px; } .exception-option { flex: 1; display: flex; align-items: center; justify-content: center; gap: 8px; padding: 15px 20px; border: 2px solid #e0e0e0; border-radius: 8px; cursor: pointer; transition: all 0.3s; background-color: #fff; &.active { border-color: #1890ff; background-color: #e6f7ff; } &:active { opacity: 0.8; } } .option-text { font-size: 14px; color: #333; font-weight: 500; } // 正常状态提示样式 .normal-tip { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 40px 20px; background-color: #f6ffed; border: 1px dashed #b7eb8f; border-radius: 8px; .tip-text { margin-top: 15px; font-size: 14px; color: #52c41a; } } </style> src/pages/inspectionUpload/index.vue
@@ -296,18 +296,22 @@ }; const getFileStatus = record => { let _beforeProduction = record.beforeProduction && record.beforeProduction.length; let _afterProduction = record.afterProduction && record.afterProduction.length; let _productionIssues = record.productionIssues && record.productionIssues.length; if (_beforeProduction && _afterProduction && _productionIssues) { return 2; } else if (_beforeProduction || _afterProduction || _productionIssues) { return 1; // 检查是否有巡检照片 (commonFileListBeforeVO) const hasFiles = (record.commonFileListBeforeVO && record.commonFileListBeforeVO.length > 0) || (record.commonFileListAfterVO && record.commonFileListAfterVO.length > 0) || (record.commonFileListVO && record.commonFileListVO.length > 0); if (hasFiles) { return 2; // 已完成 } else if ( record.inspectionResult !== undefined && record.inspectionResult !== null ) { return 1; // 巡检中 (已有结果但没照片,或者根据业务逻辑定义) } else { return 0; return 0; // 未巡检 } }; @@ -400,7 +404,12 @@ // 查看附件 - 跳转到附件页面 const viewAttachments = async task => { const taskData = encodeURIComponent(JSON.stringify(task)); // 仅传递必要的任务信息和 commonFileListBeforeVO 附件列表 const taskInfoToPass = { taskName: task.taskName, commonFileListBeforeVO: task.commonFileListBeforeVO || [], }; const taskData = encodeURIComponent(JSON.stringify(taskInfoToPass)); uni.navigateTo({ url: `/pages/inspectionUpload/attachment?taskInfo=${taskData}`, }); src/pages/inspectionUpload/upload.vue
@@ -57,33 +57,17 @@ maxlength="500" placeholder="请描述异常情况..." /> </view> <!-- 分类标签页(仅在异常时显示) --> <!-- 上传区域(仅在异常时显示) --> <view class="section-card" v-if="hasException === true"> <view class="upload-tabs"> <view class="tab-item" :class="{ active: currentUploadType === 'before' }" @click="switchUploadType('before')"> 生产前 </view> <view class="tab-item" :class="{ active: currentUploadType === 'after' }" @click="switchUploadType('after')"> 生产中 </view> <view class="tab-item" :class="{ active: currentUploadType === 'issue' }" @click="switchUploadType('issue')"> 生产后 </view> </view> <view class="section-title">巡检照片/视频</view> <!-- 当前分类的上传区域 --> <view class="upload-area"> <view class="upload-buttons"> <u-button type="primary" @click="chooseMedia('image')" :loading="uploading" :disabled="getCurrentFiles().length >= uploadConfig.limit" :disabled="beforeModelValue.length >= uploadConfig.limit" :customStyle="{ marginRight: '10px', flex: 1 }"> <u-icon name="camera" size="18" @@ -94,7 +78,7 @@ <u-button type="success" @click="chooseMedia('video')" :loading="uploading" :disabled="getCurrentFiles().length >= uploadConfig.limit" :disabled="beforeModelValue.length >= uploadConfig.limit" :customStyle="{ flex: 1 }"> <uni-icons type="videocam" size="18" @@ -111,9 +95,9 @@ activeColor="#409eff"></u-line-progress> </view> <!-- 当前分类的文件列表 --> <view v-if="getCurrentFiles().length > 0" <view v-if="beforeModelValue.length > 0" class="file-list"> <view v-for="(file, index) in getCurrentFiles()" <view v-for="(file, index) in beforeModelValue" :key="index" class="file-item"> <view class="file-preview-container"> @@ -143,17 +127,15 @@ </view> </view> </view> <view v-if="getCurrentFiles().length === 0" <view v-if="beforeModelValue.length === 0" class="empty-state"> <text>请选择要上传的{{ getUploadTypeText() }}图片或视频</text> <text>请选择要上传的图片或视频</text> </view> </view> <!-- 统计信息 --> <view class="upload-summary"> <text class="summary-text"> 生产前: {{ beforeModelValue.length }}个文件 | 生产中: {{ afterModelValue.length }}个文件 | 生产后: {{ issueModelValue.length }}个文件 已上传: {{ beforeModelValue.length }}个文件 </text> </view> </view> @@ -198,12 +180,7 @@ const uploadProgress = ref(0); // 三个分类的上传状态 const beforeModelValue = ref([]); // 生产前 const afterModelValue = ref([]); // 生产中 const issueModelValue = ref([]); // 生产后 // 当前激活的上传类型 const currentUploadType = ref("before"); // 'before', 'after', 'issue' const beforeModelValue = ref([]); // 巡检照片 // 异常状态 const hasException = ref(null); // null: 未选择, true: 存在异常, false: 正常 @@ -258,24 +235,25 @@ }); }; // 根据用户要求映射:AfterDTO(生产前), DTO(生产中), BeforeDTO(生产后) // 根据用户要求映射:仅保留生产前 if ( info.commonFileListAfterVO && Array.isArray(info.commonFileListAfterVO) ) { beforeModelValue.value = mapFiles(info.commonFileListAfterVO); } console.log(beforeModelValue.value, "beforeModelValue"); if (info.commonFileListVO && Array.isArray(info.commonFileListVO)) { afterModelValue.value = mapFiles(info.commonFileListVO); } if ( } else if ( info.commonFileListVO && Array.isArray(info.commonFileListVO) ) { beforeModelValue.value = mapFiles(info.commonFileListVO); } else if ( info.commonFileListBeforeVO && Array.isArray(info.commonFileListBeforeVO) ) { issueModelValue.value = mapFiles(info.commonFileListBeforeVO); beforeModelValue.value = mapFiles(info.commonFileListBeforeVO); } console.log(beforeModelValue.value, "beforeModelValue"); // 如果有异常描述,也恢复 if (info.abnormalDescription) { @@ -293,12 +271,7 @@ } // 自动兜底:如果存在已上传文件,则必然是异常状态,确保 UI 正常显示 if ( !hasException.value && (beforeModelValue.value.length > 0 || afterModelValue.value.length > 0 || issueModelValue.value.length > 0) ) { if (!hasException.value && beforeModelValue.value.length > 0) { hasException.value = true; } } catch (e) { @@ -310,39 +283,6 @@ // 返回上一页 const goBack = () => { uni.navigateBack(); }; // 切换上传类型 const switchUploadType = type => { currentUploadType.value = type; }; // 获取当前分类的文件列表 const getCurrentFiles = () => { switch (currentUploadType.value) { case "before": return beforeModelValue.value || []; case "after": return afterModelValue.value || []; case "issue": return issueModelValue.value || []; default: return []; } }; // 获取上传类型文本 const getUploadTypeText = () => { switch (currentUploadType.value) { case "before": return "生产前"; case "after": return "生产中"; case "issue": return "生产后"; default: return ""; } }; // 设置异常状态 @@ -361,12 +301,8 @@ hasException: hasException.value, inspectionResult: hasException.value ? 0 : 1, // 0-异常,1-正常 commonFileListAfterDTO: beforeModelValue.value, commonFileListDTO: afterModelValue.value, commonFileListBeforeDTO: issueModelValue.value, uploadedFiles: { before: beforeModelValue.value, after: afterModelValue.value, issue: issueModelValue.value, }, }; @@ -398,11 +334,7 @@ // 如果是异常状态,检查是否有上传文件和描述 if (hasException.value === true) { const totalFiles = beforeModelValue.value.length + afterModelValue.value.length + issueModelValue.value.length; if (totalFiles === 0) { if (beforeModelValue.value.length === 0) { uni.showToast({ title: "请上传异常照片", icon: "none", @@ -426,11 +358,7 @@ }); // 按照逻辑合并所有分类的文件用于提取ID const allFiles = [ ...beforeModelValue.value, ...afterModelValue.value, ...issueModelValue.value, ]; const allFiles = [...beforeModelValue.value]; // 传给后端的临时文件ID列表 let tempFileIds = []; @@ -444,8 +372,6 @@ const submitData = { ...taskInfo.value, commonFileListAfterDTO: beforeModelValue.value, // 生产前 commonFileListDTO: afterModelValue.value, // 生产中 commonFileListBeforeDTO: issueModelValue.value, // 生产后 hasException: hasException.value, inspectionResult: hasException.value ? 0 : 1, // 0-异常,1-正常 abnormalDescription: abnormalDescription.value, @@ -498,7 +424,7 @@ // 拍照/拍视频 const chooseMedia = type => { if (getCurrentFiles().length >= uploadConfig.limit) { if (beforeModelValue.value.length >= uploadConfig.limit) { uni.showToast({ title: `最多只能选择${uploadConfig.limit}个文件`, icon: "none", @@ -506,7 +432,7 @@ return; } const remaining = uploadConfig.limit - getCurrentFiles().length; const remaining = uploadConfig.limit - beforeModelValue.value.length; // 优先使用 chooseMedia if (typeof uni.chooseMedia === "function") { @@ -612,7 +538,7 @@ Authorization: `Bearer ${token}`, }, formData: { type: getTabType(), type: 10, }, success: res => { try { @@ -638,14 +564,8 @@ status: "success", }; // 根据当前类型添加到对应数组 if (currentUploadType.value === "before") { beforeModelValue.value.push(uploadedFile); } else if (currentUploadType.value === "after") { afterModelValue.value.push(uploadedFile); } else if (currentUploadType.value === "issue") { issueModelValue.value.push(uploadedFile); } // 仅添加到 beforeModelValue beforeModelValue.value.push(uploadedFile); uni.showToast({ title: "上传成功", icon: "success" }); } else { @@ -670,24 +590,9 @@ }); }; // 获取type值 const getTabType = () => { switch (currentUploadType.value) { case "before": return 10; case "after": return 11; case "issue": return 12; default: return 10; } }; // 删除文件 const removeFile = index => { const files = getCurrentFiles(); files.splice(index, 1); beforeModelValue.value.splice(index, 1); }; </script>