gaoluyang
2025-09-24 752b14d2caa47ccceac328f79389fbf5e2e62ce4
分析追溯
已添加9个文件
已修改6个文件
2800 ■■■■ 文件已修改
src/components/imageUpload/index.vue 819 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/imageUpload/viewQrCodeFiles.vue 297 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/imageUpload/viewQrCodeFilesSimple.vue 288 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/config.js 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages.json 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/equipmentManagement/faultAnalysis/index.vue 353 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/equipmentManagement/repair/add.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/index.vue 18 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/inspectionUpload/components/qrCodeFormDia.vue 228 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/inspectionUpload/index.vue 787 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/static/images/icon/guzhangfenxi@2x.png 补丁 | 查看 | 原始文档 | blame | 历史
src/static/images/icon/jieguoyanzheng@2x.png 补丁 | 查看 | 原始文档 | blame | 历史
src/static/images/icon/xunjianshangchuan@2x.png 补丁 | 查看 | 原始文档 | blame | 历史
src/static/images/icon/zhinengpaidan@2x.png 补丁 | 查看 | 原始文档 | blame | 历史
src/static/images/icon/zuoyezhidao@2x.png 补丁 | 查看 | 原始文档 | blame | 历史
src/components/imageUpload/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,819 @@
<template>
  <view class="camera-upload">
    <!-- æ‹ç…§/拍视频按钮 -->
    <view v-if="!disabled" class="camera-buttons">
      <view class="button-row">
        <u-button
          type="primary"
          @click="takePhoto"
          :loading="uploading"
          :disabled="fileList.length >= limit"
          :customStyle="{ marginRight: '10px', flex: 1 }"
        >
          <u-icon name="camera" size="18" color="#fff" style="margin-right: 5px;"></u-icon>
          {{ uploading ? '上传中...' : '拍照' }}
        </u-button>
        <u-button
          type="success"
          @click="takeVideo"
          :loading="uploading"
          :disabled="fileList.length >= limit"
          :customStyle="{ flex: 1 }"
        >
          <u-icon name="video" size="18" color="#fff" style="margin-right: 5px;"></u-icon>
          {{ uploading ? '上传中...' : '拍视频' }}
        </u-button>
      </view>
    </view>
    <!-- æç¤ºä¿¡æ¯ -->
    <view v-if="showTip && !disabled" class="upload-tip">
      è¯·ä½¿ç”¨ç›¸æœº
      <text v-if="fileSize" class="tip-text">
        æ‹æ‘„大小不超过 <text class="tip-highlight">{{ fileSize }}MB</text>
      </text>
      çš„
      <text class="tip-highlight">照片或视频</text>
    </view>
    <!-- åª’体文件列表 -->
    <view class="media-list">
      <view
        v-for="(file, index) in fileList"
        :key="file.uid || index"
        class="media-item"
      >
        <!-- é¢„览区域 -->
        <view class="media-preview" @click="previewMedia(file, index)">
          <image
            v-if="file.type === 'image'"
            :src="file.url || file.tempFilePath"
            class="preview-image"
            mode="aspectFill"
          ></image>
          <video
            v-else-if="file.type === 'video'"
            :src="file.url || file.tempFilePath"
            class="preview-video"
            :controls="false"
          ></video>
          <view class="media-type-icon">
            <u-icon
              :name="file.type === 'image' ? 'photo' : 'video'"
              size="12"
              color="#fff"
            ></u-icon>
          </view>
        </view>
        <!-- æ“ä½œæŒ‰é’® -->
        <view class="media-actions" v-if="!disabled">
          <u-button
            type="error"
            size="mini"
            @click="handleDelete(index)"
            :customStyle="{
              minWidth: '40px',
              height: '24px',
              fontSize: '10px',
              padding: '0 8px'
            }"
          >
            åˆ é™¤
          </u-button>
        </view>
      </view>
    </view>
    <!-- ä¸Šä¼ è¿›åº¦ -->
    <view v-if="uploading" class="upload-progress">
      <u-line-progress
        :percentage="uploadProgress"
        :showText="true"
        activeColor="#409eff"
      ></u-line-progress>
    </view>
  </view>
</template>
<script setup>
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue';
import { getToken } from "@/utils/auth";
// Props å®šä¹‰
const props = defineProps({
  modelValue: [String, Object, Array],
  action: { type: String, default: "/common/minioUploads" },
  data: { type: Object },
  limit: { type: Number, default: 5 },
  fileSize: { type: Number, default: 10 }, // é»˜è®¤10MB,适合视频
  fileType: {
    type: Array,
    default: () => ["jpg", "jpeg", "png", "mp4", "mov"]
  },
  isShowTip: { type: Boolean, default: true },
  disabled: { type: Boolean, default: false },
  drag: { type: Boolean, default: false }, // æ‹ç…§ä¸éœ€è¦æ‹–拽
  statusType: { type: Number, default: "" }, // ç”¨äºŽåŒºåˆ†ä¸åŒçŠ¶æ€çš„ä¸Šä¼ 
  maxVideoDuration: { type: Number, default: 30 }, // æœ€å¤§è§†é¢‘时长(秒)
});
// äº‹ä»¶å®šä¹‰
const emit = defineEmits(['update:modelValue']);
// å“åº”式数据
const number = ref(0);
const uploadList = ref([]);
const fileList = ref([]);
const uploading = ref(false);
const uploadProgress = ref(0);
// è®¡ç®—属性
const uploadFileUrl = computed(() => {
  // èŽ·å–åŸºç¡€API地址,适配uniapp环境
  let baseUrl = '';
  // å°è¯•多种方式获取baseUrl
  if (process.env.VUE_APP_BASE_API) {
    baseUrl = process.env.VUE_APP_BASE_API;
  } else if (process.env.NODE_ENV === 'development') {
    baseUrl = 'http://192.168.1.147:9036';
  } else {
    baseUrl = 'http://192.168.1.147:9036';
  }
  const fullUrl = baseUrl + props.action;
  return fullUrl;
});
const headers = computed(() => {
  const token = getToken();
  return token ? { Authorization: "Bearer " + token } : {};
});
const showTip = computed(() => props.isShowTip && (props.fileType || props.fileSize));
// åˆå§‹åŒ–和编辑初始化方法
const init = () => {
  fileList.value = [];
  uploadList.value = [];
  number.value = 0;
};
const editInit = (val) => {
  fileList.value = [];
  val.storageBlobDTO.forEach((element) => {
    // ç¡®ä¿æ–‡ä»¶æ•°æ®åŒ…含所有必要字段,包括id
    const fileData = {
      ...element,
      id: element.id, // ä¿ç•™æœåŠ¡å™¨è¿”å›žçš„id
      url: element.url || element.downloadUrl,
      bucketFilename: element.bucketFilename || element.name,
      downloadUrl: element.downloadUrl || element.url,
      type: element.type || (element.url && element.url.includes('video') ? 'video' : 'image'),
      name: element.name || element.bucketFilename || `文件_${Date.now()}`,
      size: element.size || 0,
      createTime: element.createTime || new Date().getTime(),
      uid: element.uid || new Date().getTime() + Math.random()
    };
    fileList.value.push(fileData);
    uploadedSuccessfully();
  });
};
// æµ‹è¯•服务器连接
const testServerConnection = () => {
  return new Promise((resolve) => {
    uni.request({
      url: uploadFileUrl.value.replace('/common/minioUploads', '/common/test'),
      method: 'GET',
      timeout: 5000,
      success: (res) => {
        resolve(true);
      },
      fail: (err) => {
        resolve(false);
      }
    });
  });
};
// ç»„件销毁时的清理
onUnmounted(() => {
  // æ¸…理上传状态
  if (uploading.value) {
    uploading.value = false
  }
  // éšè—å¯èƒ½æ˜¾ç¤ºçš„加载提示
  uni.hideLoading()
  uni.hideToast()
})
// æš´éœ²æ–¹æ³•
defineExpose({ init, editInit, testServerConnection });
// ç›‘听 modelValue å˜åŒ–
watch(
  () => props.modelValue,
  (val) => {
    if (val) {
      let temp = 1;
      let list = [];
      if (Array.isArray(val)) {
        list = val;
      } else if (typeof val === "string") {
        list = val.split(",").map(url => ({ url: url.trim() }));
      }
      fileList.value = list.map((item) => {
        if (typeof item === "string") {
          item = { name: item, url: item };
        }
        // ç¡®ä¿æ¯ä¸ªæ–‡ä»¶éƒ½æœ‰å¿…要的属性,包括id
        return {
          ...item,
          id: item.id, // ä¿ç•™id字段
          uid: item.uid || new Date().getTime() + temp++,
          type: item.type || (item.url && item.url.includes('video') ? 'video' : 'image'),
          name: item.name || item.bucketFilename || `文件_${Date.now()}`,
          size: item.size || 0,
          createTime: item.createTime || new Date().getTime()
        };
      });
    } else {
      fileList.value = [];
    }
  },
  { deep: true, immediate: true }
);
// æ‹ç…§
const takePhoto = () => {
  if (fileList.value.length >= props.limit) {
    uni.showToast({
      title: `最多只能拍摄${props.limit}个文件`,
      icon: 'none'
    });
    return;
  }
  uni.chooseImage({
    count: 1,
    sizeType: ['compressed', 'original'],
    sourceType: ['camera'],
    success: (res) => {
      try {
        if (!res.tempFilePaths || res.tempFilePaths.length === 0) {
          throw new Error('未获取到图片文件');
        }
        const tempFilePath = res.tempFilePaths[0];
        const tempFile = res.tempFiles && res.tempFiles[0] ? res.tempFiles[0] : {};
        const file = {
          tempFilePath: tempFilePath,
          type: 'image',
          name: `photo_${Date.now()}.jpg`,
          size: tempFile.size || 0,
          createTime: new Date().getTime(),
          uid: Date.now() + Math.random()
        };
        handleBeforeUpload(file);
      } catch (error) {
        console.error('处理拍照结果失败:', error);
        uni.showToast({
          title: '处理图片失败',
          icon: 'error'
        });
      }
    },
    fail: (err) => {
      console.error('拍照失败:', err);
      uni.showToast({
        title: '拍照失败: ' + (err.errMsg || '未知错误'),
        icon: 'error'
      });
    }
  });
};
// æ‹è§†é¢‘
const takeVideo = () => {
  if (fileList.value.length >= props.limit) {
    uni.showToast({
      title: `最多只能拍摄${props.limit}个文件`,
      icon: 'none'
    });
    return;
  }
  uni.chooseVideo({
    sourceType: ['camera'],
    maxDuration: props.maxVideoDuration,
    camera: 'back',
    success: (res) => {
      try {
        if (!res.tempFilePath) {
          throw new Error('未获取到视频文件');
        }
        const file = {
          tempFilePath: res.tempFilePath,
          type: 'video',
          name: `video_${Date.now()}.mp4`,
          size: res.size || 0,
          duration: res.duration || 0,
          createTime: new Date().getTime(),
          uid: Date.now() + Math.random()
        };
        handleBeforeUpload(file);
      } catch (error) {
        console.error('处理拍视频结果失败:', error);
        uni.showToast({
          title: '处理视频失败',
          icon: 'error'
        });
      }
    },
    fail: (err) => {
      console.error('拍视频失败:', err);
      uni.showToast({
        title: '拍视频失败: ' + (err.errMsg || '未知错误'),
        icon: 'error'
      });
    }
  });
};
// æ–‡ä»¶ä¸Šä¼ å¤„理
const uploadFile = (file) => {
  uploading.value = true;
  uploadProgress.value = 0;
  number.value++; // å¢žåŠ ä¸Šä¼ è®¡æ•°
  // ç¡®ä¿æ–‡ä»¶è·¯å¾„正确
  const filePath = file.tempFilePath || file.path;
  if (!filePath) {
    handleUploadError('文件路径不存在');
    return;
  }
  // ç¡®ä¿token存在
  const token = getToken();
  if (!token) {
    handleUploadError('用户未登录');
    return;
  }
  // å‡†å¤‡ä¸Šä¼ å‚æ•°
  const uploadParams = {
    url: uploadFileUrl.value,
    filePath: filePath,
    name: 'files',
    formData: {
      type: props.statusType || 0,
      ...(props.data || {})
    },
    header: {
      'Authorization': `Bearer ${token}`
    }
  };
  const uploadTask = uni.uploadFile({
    ...uploadParams,
    success: (res) => {
      try {
        if (res.statusCode === 200) {
          const response = JSON.parse(res.data);
          if (response.code === 200) {
            handleUploadSuccess(response, file);
            uni.showToast({
              title: '上传成功',
              icon: 'success'
            });
            emit("update:modelValue", fileList.value);
          } else {
            handleUploadError(response.msg || '服务器返回错误');
          }
        } else {
          handleUploadError(`服务器错误,状态码: ${res.statusCode}`);
        }
      } catch (e) {
        console.error('解析响应失败:', e);
        console.error('原始响应数据:', res.data);
        handleUploadError('响应数据解析失败: ' + e.message);
      }
    },
    fail: (err) => {
      console.error('上传失败:', err.errMsg || err);
      number.value--; // ä¸Šä¼ å¤±è´¥æ—¶å‡å°‘计数
      let errorMessage = '上传失败';
      if (err.errMsg) {
        if (err.errMsg.includes('statusCode: null')) {
          errorMessage = '网络连接失败,请检查网络设置';
        } else if (err.errMsg.includes('timeout')) {
          errorMessage = '上传超时,请重试';
        } else if (err.errMsg.includes('fail')) {
          errorMessage = '上传失败,请检查网络连接';
        } else {
          errorMessage = err.errMsg;
        }
      }
      handleUploadError(errorMessage);
    },
    complete: () => {
      uploading.value = false;
      uploadProgress.value = 0;
    }
  });
  // ç›‘听上传进度
  if (uploadTask && uploadTask.onProgressUpdate) {
    uploadTask.onProgressUpdate((res) => {
      uploadProgress.value = res.progress;
    });
  }
};
// èŽ·å–åª’ä½“æ–‡ä»¶å
const getMediaName = (file) => {
  if (file.bucketFilename) {
    return file.bucketFilename.length > 15
      ? file.bucketFilename.substring(0, 15) + '...'
      : file.bucketFilename;
  }
  if (file.name) {
    return file.name.length > 15
      ? file.name.substring(0, 15) + '...'
      : file.name;
  }
  return file.type === 'image' ? '照片' : '视频';
};
// æ ¼å¼åŒ–文件大小
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 formatTime = (timestamp) => {
  if (!timestamp) return '';
  const date = new Date(timestamp);
  const now = new Date();
  const diff = now - date;
  if (diff < 60000) return '刚刚';
  if (diff < 3600000) return Math.floor(diff / 60000) + '分钟前';
  if (diff < 86400000) return Math.floor(diff / 3600000) + '小时前';
  return date.toLocaleDateString();
};
// é¢„览媒体文件
const previewMedia = (file, index) => {
  if (file.type === 'image') {
    // é¢„览图片
    const urls = fileList.value
      .filter(item => item.type === 'image')
      .map(item => item.url || item.tempFilePath);
    uni.previewImage({
      urls: urls,
      current: file.url || file.tempFilePath
    });
  } else if (file.type === 'video') {
    // é¢„览视频
    uni.previewVideo({
      sources: [{
        src: file.url || file.tempFilePath,
        type: 'mp4'
      }],
      current: 0
    });
  }
};
// ä¸‹è½½æ–‡ä»¶
const handleDownload = (index) => {
  const file = fileList.value[index];
  const url = file.url || file.downloadUrl;
  if (!url) {
    uni.showToast({
      title: '文件链接不存在,无法下载',
      icon: 'none'
    });
    return;
  }
  // ä½¿ç”¨uniapp的下载API
  uni.downloadFile({
    url: url,
    success: (res) => {
      if (res.statusCode === 200) {
        // ä¿å­˜åˆ°ç›¸å†Œæˆ–文件系统
        uni.saveFile({
          tempFilePath: res.tempFilePath,
          success: (saveRes) => {
            uni.showToast({
              title: '下载成功',
              icon: 'success'
            });
          },
          fail: (err) => {
            console.error('保存文件失败:', err);
            uni.showToast({
              title: '保存文件失败',
              icon: 'error'
            });
          }
        });
      }
    },
    fail: (err) => {
      console.error('下载失败:', err);
      uni.showToast({
        title: '下载失败',
        icon: 'error'
      });
    }
  });
};
// æ£€æŸ¥ç½‘络连接
const checkNetworkConnection = () => {
  return new Promise((resolve) => {
    uni.getNetworkType({
      success: (res) => {
        if (res.networkType === 'none') {
          resolve(false);
        } else {
          resolve(true);
        }
      },
      fail: () => {
        resolve(false);
      }
    });
  });
};
// ä¸Šä¼ å‰æ ¡éªŒ
const handleBeforeUpload = async (file) => {
  // æ£€æŸ¥ç½‘络连接
  const hasNetwork = await checkNetworkConnection();
  if (!hasNetwork) {
    uni.showToast({
      title: '网络连接不可用,请检查网络设置',
      icon: 'none'
    });
    return false;
  }
  // æ ¡éªŒæ–‡ä»¶å¤§å°
  if (props.fileSize && file.size) {
    const isLt = file.size / 1024 / 1024 < props.fileSize;
    if (!isLt) {
      uni.showToast({
        title: `文件大小不能超过 ${props.fileSize} MB!`,
        icon: 'none'
      });
      return false;
    }
  }
  // æ ¡éªŒè§†é¢‘æ—¶é•¿
  if (file.type === 'video' && file.duration && file.duration > props.maxVideoDuration) {
    uni.showToast({
      title: `视频时长不能超过 ${props.maxVideoDuration} ç§’!`,
      icon: 'none'
    });
    return false;
  }
  // æ ¡éªŒæ–‡ä»¶ç±»åž‹
  if (props.fileType && Array.isArray(props.fileType) && props.fileType.length > 0) {
    const fileName = file.name || '';
    const fileExtension = fileName ? fileName.split('.').pop().toLowerCase() : '';
    // æ ¹æ®æ–‡ä»¶ç±»åž‹ç¡®å®šæœŸæœ›çš„æ‰©å±•名
    let expectedTypes = [];
    if (file.type === 'image') {
      expectedTypes = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
    } else if (file.type === 'video') {
      expectedTypes = ['mp4', 'mov', 'avi', 'wmv'];
    }
    // æ£€æŸ¥æ–‡ä»¶æ‰©å±•名是否在允许的类型中
    if (fileExtension && expectedTypes.length > 0) {
      const isAllowed = expectedTypes.some(type =>
        props.fileType.includes(type) && type === fileExtension
      );
      if (!isAllowed) {
        uni.showToast({
          title: `文件格式不支持,请拍摄 ${expectedTypes.join('/')} æ ¼å¼çš„æ–‡ä»¶`,
          icon: 'none'
        });
        return false;
      }
    }
  }
  // æ ¡éªŒé€šè¿‡ï¼Œå¼€å§‹ä¸Šä¼ 
  uploadFile(file);
  return true;
};
// ä¸Šä¼ å¤±è´¥å¤„理
const handleUploadError = (message = '上传文件失败', showRetry = true) => {
  if (showRetry) {
    uni.showModal({
      title: '上传失败',
      content: message + ',是否重试?',
      success: (res) => {
        if (res.confirm) {
          // ç”¨æˆ·é€‰æ‹©é‡è¯•,这里可以重新触发上传
        }
      }
    });
  } else {
    uni.showToast({
      title: message,
      icon: 'error'
    });
  }
};
// ä¸Šä¼ æˆåŠŸå›žè°ƒ
const handleUploadSuccess = (res, file) => {
  if (res.code === 200 && res.data && Array.isArray(res.data) && res.data.length > 0) {
    const uploadedFile = res.data[0];
    // ç¡®ä¿ä¸Šä¼ çš„æ–‡ä»¶æ•°æ®å®Œæ•´ï¼ŒåŒ…含id
    const fileData = {
      ...file,
      id: uploadedFile.id, // æ·»åŠ æœåŠ¡å™¨è¿”å›žçš„id
      url: uploadedFile.url || uploadedFile.downloadUrl,
      bucketFilename: uploadedFile.bucketFilename || file.name,
      downloadUrl: uploadedFile.downloadUrl || uploadedFile.url,
      size: uploadedFile.size || file.size,
      createTime: uploadedFile.createTime || new Date().getTime()
    };
    uploadList.value.push(fileData);
    uploadedSuccessfully();
  } else {
    number.value--; // ä¸Šä¼ å¤±è´¥æ—¶å‡å°‘计数
    handleUploadError(res.msg || '上传失败');
  }
};
// åˆ é™¤æ–‡ä»¶
const handleDelete = (index) => {
  uni.showModal({
    title: '确认删除',
    content: '确定要删除这个文件吗?',
    success: (res) => {
      if (res.confirm) {
        fileList.value.splice(index, 1);
        emit("update:modelValue", listToString(fileList.value));
        uni.showToast({
          title: '删除成功',
          icon: 'success'
        });
      }
    }
  });
};
// ä¸Šä¼ ç»“束处理
const uploadedSuccessfully = () => {
  if (number.value > 0 && uploadList.value.length === number.value) {
    // åˆå¹¶å·²å­˜åœ¨çš„æ–‡ä»¶å’Œåˆšä¸Šä¼ çš„æ–‡ä»¶
    const existingFiles = fileList.value.filter((f) => f.url !== undefined);
    fileList.value = [...existingFiles, ...uploadList.value];
    // é‡ç½®çŠ¶æ€
    uploadList.value = [];
    number.value = 0;
    // è§¦å‘更新事件,传递完整的文件列表
    emit("update:modelValue", fileList.value);
  }
};
const listToString = (list, separator = ",") => {
  const strs = list
    .filter(item => item.url)
    .map(item => item.url)
    .join(separator);
  return strs;
};
</script>
<style scoped lang="scss">
.camera-upload {
  width: 100%;
}
.camera-buttons {
  margin-bottom: 15px;
  .button-row {
    display: flex;
    gap: 10px;
  }
}
.upload-tip {
  font-size: 12px;
  color: #909399;
  margin-bottom: 15px;
  text-align: center;
  line-height: 1.5;
  .tip-text {
    margin: 0 2px;
  }
  .tip-highlight {
    color: #f56c6c;
    font-weight: bold;
  }
}
.media-list {
  margin-top: 10px;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
.media-item {
  position: relative;
  width: 80px;
  height: 80px;
  border-radius: 8px;
  overflow: hidden;
  background-color: #f5f5f5;
  border: 2px solid #e9ecef;
  transition: all 0.3s ease;
  &:hover {
    border-color: #409eff;
    transform: scale(1.02);
  }
}
.media-preview {
  position: relative;
  width: 100%;
  height: 100%;
  cursor: pointer;
  .preview-image, .preview-video {
    width: 100%;
    height: 100%;
    object-fit: cover;
  }
  .media-type-icon {
    position: absolute;
    top: 4px;
    right: 4px;
    width: 20px;
    height: 20px;
    background-color: rgba(0, 0, 0, 0.6);
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
  }
}
.media-actions {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
  padding: 4px;
  display: flex;
  justify-content: center;
  opacity: 0;
  transition: opacity 0.3s ease;
  .media-item:hover & {
    opacity: 1;
  }
}
.upload-progress {
  margin-top: 15px;
  padding: 0 10px;
}
</style>
src/components/imageUpload/viewQrCodeFiles.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,297 @@
<template>
  <view>
    <!-- å¼¹çª— -->
    <u-popup
      v-model="dialogVisitable"
      mode="center"
      width="90%"
      height="80%"
      border-radius="20"
      @close="cancel"
    >
      <view class="popup-content">
        <view class="popup-header">
          <text class="popup-title">查看附件</text>
          <u-icon
            name="close"
            size="24"
            color="#999"
            @click="cancel"
          ></u-icon>
        </view>
        <view class="upload-container">
          <view class="form-container">
            <view class="title">巡检附件</view>
            <!-- å›¾ç‰‡åˆ—表 -->
            <view v-if="beforeProductionImgs.length > 0" class="media-section">
              <view class="section-title">图片</view>
              <view class="image-grid">
                <view
                  v-for="(item, index) in beforeProductionImgs"
                  :key="index"
                  class="image-item"
                  @click="previewImage(item, index)"
                >
                  <image
                    :src="item"
                    mode="aspectFill"
                    class="image-preview"
                  />
                </view>
              </view>
            </view>
            <!-- è§†é¢‘列表 -->
            <view v-if="beforeProductionVideos.length > 0" class="media-section">
              <view class="section-title">视频</view>
              <view class="video-grid">
                <view
                  v-for="(videoUrl, index) in beforeProductionVideos"
                  :key="index"
                  class="video-item"
                  @click="previewVideo(videoUrl)"
                >
                  <view class="video-preview">
                    <u-icon name="play-circle-fill" size="40" color="#fff"></u-icon>
                  </view>
                  <view class="video-tip">点击播放</view>
                </view>
              </view>
            </view>
            <!-- ç©ºçŠ¶æ€ -->
            <view v-if="beforeProductionImgs.length === 0 && beforeProductionVideos.length === 0" class="empty-state">
              <u-empty
                mode="data"
                text="暂无附件"
                :iconSize="60"
              ></u-empty>
            </view>
          </view>
        </view>
      </view>
    </u-popup>
  </view>
</template>
<script setup>
import { ref, onUnmounted } from 'vue'
// æŽ§åˆ¶å¼¹çª—显示
const dialogVisitable = ref(false)
// å›¾ç‰‡æ•°ç»„
const beforeProductionImgs = ref([])
// è§†é¢‘数组
const beforeProductionVideos = ref([])
// æ‰“开弹窗并加载数据
const openDialog = async (row) => {
  console.log('打开附件查看弹窗,数据:', row)
  // å¤„理数据,分离图片和视频
  const { images: beforeImgs, videos: beforeVids } = processItems(row.storageBlobDTO || [])
  beforeProductionImgs.value = beforeImgs
  beforeProductionVideos.value = beforeVids
  dialogVisitable.value = true
}
// é¢„览图片
const previewImage = (url, index) => {
  uni.previewImage({
    urls: beforeProductionImgs.value,
    current: index,
    fail: (err) => {
      console.error('图片预览失败:', err)
      uni.showToast({
        title: '图片预览失败',
        icon: 'error'
      })
    }
  })
}
// é¢„览视频
const previewVideo = (url) => {
  uni.previewVideo({
    sources: [{
      src: url
    }],
    fail: (err) => {
      console.error('视频预览失败:', err)
      uni.showToast({
        title: '视频预览失败',
        icon: 'error'
      })
    }
  })
}
// è¡¨å•关闭方法
const cancel = () => {
  dialogVisitable.value = false
  // é‡ç½®æ•°æ®
  beforeProductionImgs.value = []
  beforeProductionVideos.value = []
}
// å¤„理每一类数据:分离图片和视频
function processItems(items) {
  if (!items || !Array.isArray(items)) {
    return { images: [], videos: [] }
  }
  const images = []
  const videos = []
  items.forEach(item => {
    if (item.contentType?.startsWith('image/')) {
      images.push(item.url)
    } else if (item.contentType?.startsWith('video/')) {
      videos.push(item.url)
    }
  })
  return { images, videos }
}
// ç»„件销毁时的清理
onUnmounted(() => {
  // å…³é—­å¼¹çª—
  dialogVisitable.value = false
  // æ¸…理数据
  beforeProductionImgs.value = []
  beforeProductionVideos.value = []
})
// æš´éœ²æ–¹æ³•给父组件
defineExpose({ openDialog })
</script>
<style scoped lang="scss">
.popup-content {
  height: 100%;
  display: flex;
  flex-direction: column;
}
.popup-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 20px;
  border-bottom: 1px solid #f0f0f0;
}
.popup-title {
  font-size: 18px;
  font-weight: 600;
  color: #333;
}
.upload-container {
  flex: 1;
  padding: 20px;
  overflow-y: auto;
}
.form-container {
  width: 100%;
}
.title {
  font-size: 16px;
  color: #1890ff;
  line-height: 24px;
  font-weight: 600;
  padding-left: 12px;
  position: relative;
  margin: 0 0 20px 0;
  &::before {
    content: "";
    position: absolute;
    left: 0;
    top: 4px;
    width: 4px;
    height: 16px;
    background-color: #1890ff;
    border-radius: 2px;
  }
}
.media-section {
  margin-bottom: 30px;
}
.section-title {
  font-size: 14px;
  color: #666;
  margin-bottom: 15px;
  font-weight: 500;
}
.image-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}
.image-item {
  width: 100px;
  height: 100px;
  border-radius: 8px;
  overflow: hidden;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.image-preview {
  width: 100%;
  height: 100%;
}
.video-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 15px;
}
.video-item {
  width: 160px;
  height: 90px;
  border-radius: 8px;
  overflow: hidden;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.video-preview {
  width: 100%;
  height: 100%;
  background-color: #333;
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
}
.video-tip {
  position: absolute;
  bottom: 5px;
  left: 50%;
  transform: translateX(-50%);
  font-size: 12px;
  color: #fff;
  background-color: rgba(0, 0, 0, 0.6);
  padding: 2px 8px;
  border-radius: 4px;
}
.empty-state {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 200px;
}
</style>
src/components/imageUpload/viewQrCodeFilesSimple.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,288 @@
<template>
  <view>
    <!-- å¼¹çª— -->
    <u-popup
      v-model="dialogVisitable"
      mode="center"
      width="90%"
      height="80%"
      border-radius="20"
      @close="cancel"
    >
      <view class="popup-content">
        <view class="popup-header">
          <text class="popup-title">查看附件</text>
          <u-icon
            name="close"
            size="24"
            color="#999"
            @click="cancel"
          ></u-icon>
        </view>
        <view class="upload-container">
          <view class="form-container">
            <view class="title">巡检附件</view>
            <!-- å›¾ç‰‡åˆ—表 -->
            <view v-if="beforeProductionImgs.length > 0" class="media-section">
              <view class="section-title">图片</view>
              <view class="image-grid">
                <view
                  v-for="(item, index) in beforeProductionImgs"
                  :key="index"
                  class="image-item"
                  @click="previewImage(item, index)"
                >
                  <image
                    :src="item"
                    mode="aspectFill"
                    class="image-preview"
                  />
                </view>
              </view>
            </view>
            <!-- è§†é¢‘列表 -->
            <view v-if="beforeProductionVideos.length > 0" class="media-section">
              <view class="section-title">视频</view>
              <view class="video-grid">
                <view
                  v-for="(videoUrl, index) in beforeProductionVideos"
                  :key="index"
                  class="video-item"
                  @click="previewVideo(videoUrl)"
                >
                  <view class="video-preview">
                    <u-icon name="play-circle-fill" size="40" color="#fff"></u-icon>
                  </view>
                  <view class="video-tip">点击播放</view>
                </view>
              </view>
            </view>
            <!-- ç©ºçŠ¶æ€ -->
            <view v-if="beforeProductionImgs.length === 0 && beforeProductionVideos.length === 0" class="empty-state">
              <u-empty
                mode="data"
                text="暂无附件"
                :iconSize="60"
              ></u-empty>
            </view>
          </view>
        </view>
      </view>
    </u-popup>
  </view>
</template>
<script setup>
import { ref } from 'vue'
// æŽ§åˆ¶å¼¹çª—显示
const dialogVisitable = ref(false)
// å›¾ç‰‡æ•°ç»„
const beforeProductionImgs = ref([])
// è§†é¢‘数组
const beforeProductionVideos = ref([])
// æ‰“开弹窗并加载数据
const openDialog = async (row) => {
  console.log('打开附件查看弹窗,数据:', row)
  // å¤„理数据,分离图片和视频
  const { images: beforeImgs, videos: beforeVids } = processItems(row.storageBlobDTO || [])
  beforeProductionImgs.value = beforeImgs
  beforeProductionVideos.value = beforeVids
  dialogVisitable.value = true
}
// é¢„览图片
const previewImage = (url, index) => {
  uni.previewImage({
    urls: beforeProductionImgs.value,
    current: index,
    fail: (err) => {
      console.error('图片预览失败:', err)
      uni.showToast({
        title: '图片预览失败',
        icon: 'error'
      })
    }
  })
}
// é¢„览视频
const previewVideo = (url) => {
  uni.previewVideo({
    sources: [{
      src: url
    }],
    fail: (err) => {
      console.error('视频预览失败:', err)
      uni.showToast({
        title: '视频预览失败',
        icon: 'error'
      })
    }
  })
}
// è¡¨å•关闭方法
const cancel = () => {
  dialogVisitable.value = false
  // é‡ç½®æ•°æ®
  beforeProductionImgs.value = []
  beforeProductionVideos.value = []
}
// å¤„理每一类数据:分离图片和视频
function processItems(items) {
  if (!items || !Array.isArray(items)) {
    return { images: [], videos: [] }
  }
  const images = []
  const videos = []
  items.forEach(item => {
    if (item.contentType?.startsWith('image/')) {
      images.push(item.url)
    } else if (item.contentType?.startsWith('video/')) {
      videos.push(item.url)
    }
  })
  return { images, videos }
}
// æš´éœ²æ–¹æ³•给父组件
defineExpose({ openDialog })
</script>
<style scoped lang="scss">
.popup-content {
  height: 100%;
  display: flex;
  flex-direction: column;
}
.popup-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 20px;
  border-bottom: 1px solid #f0f0f0;
}
.popup-title {
  font-size: 18px;
  font-weight: 600;
  color: #333;
}
.upload-container {
  flex: 1;
  padding: 20px;
  overflow-y: auto;
}
.form-container {
  width: 100%;
}
.title {
  font-size: 16px;
  color: #1890ff;
  line-height: 24px;
  font-weight: 600;
  padding-left: 12px;
  position: relative;
  margin: 0 0 20px 0;
  &::before {
    content: "";
    position: absolute;
    left: 0;
    top: 4px;
    width: 4px;
    height: 16px;
    background-color: #1890ff;
    border-radius: 2px;
  }
}
.media-section {
  margin-bottom: 30px;
}
.section-title {
  font-size: 14px;
  color: #666;
  margin-bottom: 15px;
  font-weight: 500;
}
.image-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}
.image-item {
  width: 100px;
  height: 100px;
  border-radius: 8px;
  overflow: hidden;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.image-preview {
  width: 100%;
  height: 100%;
}
.video-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 15px;
}
.video-item {
  width: 160px;
  height: 90px;
  border-radius: 8px;
  overflow: hidden;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.video-preview {
  width: 100%;
  height: 100%;
  background-color: #333;
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
}
.video-tip {
  position: absolute;
  bottom: 5px;
  left: 50%;
  transform: translateX(-50%);
  font-size: 12px;
  color: #fff;
  background-color: rgba(0, 0, 0, 0.6);
  padding: 2px 8px;
  border-radius: 4px;
}
.empty-state {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 200px;
}
</style>
src/config.js
@@ -3,6 +3,7 @@
  //  baseUrl: 'https://vue.ruoyi.vip/prod-api',
  // baseUrl: 'http://localhost/prod-api',
  baseUrl: 'http://114.132.189.42:9036',
  // baseUrl: 'http://192.168.1.147:9036',
   //cloud后台网关地址
  //  baseUrl: 'http://192.168.10.3:8080',
   // åº”用信息
src/pages.json
@@ -392,6 +392,13 @@
        "navigationBarTitleText": "巡检上传",
        "navigationStyle": "custom"
      }
    },
    {
      "path": "pages/equipmentManagement/faultAnalysis/index",
      "style": {
        "navigationBarTitleText": "故障分析追溯",
        "navigationStyle": "custom"
      }
    }
  ],
  "subPackages": [
src/pages/equipmentManagement/faultAnalysis/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,353 @@
<template>
  <view class="fault-analysis-page">
    <!-- é¡µé¢å¤´éƒ¨ -->
    <PageHeader title="故障分析追溯" @back="goBack" />
    <!-- ç»Ÿè®¡æ¦‚览 -->
    <view class="overview-section">
      <view class="section-item">
        <view class="item-header">
          <view class="item-left">
            <view class="document-icon">
              <up-icon name="file-text" size="16" color="#ffffff"></up-icon>
            </view>
            <text class="item-id">统计概览</text>
          </view>
        </view>
        <up-divider></up-divider>
        <view class="overview-content">
          <view class="overview-item">
            <view class="overview-number">{{ overviewData.totalFaults }}</view>
            <view class="overview-label">故障总数</view>
          </view>
          <view class="overview-item">
            <view class="overview-number">{{ overviewData.totalDowntime }}</view>
            <view class="overview-label">总停机时长(h)</view>
          </view>
          <view class="overview-item">
            <view class="overview-number">{{ overviewData.avgRepairTime }}</view>
            <view class="overview-label">平均修复时间(h)</view>
          </view>
          <view class="overview-item">
            <view class="overview-number">{{ overviewData.faultRate }}%</view>
            <view class="overview-label">故障率</view>
          </view>
        </view>
      </view>
    </view>
    <!-- æ•…障类型统计 -->
    <view class="stat-section">
      <view class="section-item">
        <view class="item-header">
          <view class="item-left">
            <view class="document-icon">
              <up-icon name="file-text" size="16" color="#ffffff"></up-icon>
            </view>
            <text class="item-id">故障类型统计</text>
          </view>
        </view>
        <up-divider></up-divider>
        <view class="item-details">
          <view class="detail-row" v-for="(item, index) in faultTypeStats" :key="index">
            <text class="detail-label">{{ item.name }}</text>
            <text class="detail-value">{{ item.value }}次</text>
            <text class="detail-value highlight">{{ item.percent }}%</text>
          </view>
        </view>
      </view>
    </view>
    <!-- æ ¹å› åˆ†æžç»Ÿè®¡ -->
    <view class="stat-section">
      <view class="section-item">
        <view class="item-header">
          <view class="item-left">
            <view class="document-icon">
              <up-icon name="search" size="16" color="#ffffff"></up-icon>
            </view>
            <text class="item-id">根因分析统计</text>
          </view>
        </view>
        <up-divider></up-divider>
        <view class="item-details">
          <view class="detail-row" v-for="(item, index) in rootCauseStats" :key="index">
            <text class="detail-label">{{ item.name }}</text>
            <text class="detail-value">{{ item.value }}次</text>
            <text class="detail-value highlight">{{ item.percent }}%</text>
          </view>
        </view>
      </view>
    </view>
    <!-- è¯¦ç»†æ•°æ®åˆ—表 -->
    <view class="table-section">
      <view class="section-item">
        <view class="item-header">
          <view class="item-left">
            <view class="document-icon">
              <up-icon name="list" size="16" color="#ffffff"></up-icon>
            </view>
            <text class="item-id">详细数据</text>
          </view>
        </view>
        <up-divider></up-divider>
        <view class="fault-list">
          <view v-for="(item, index) in tableData" :key="index" class="fault-item">
            <view class="item-details">
              <view class="detail-row">
                <text class="detail-label">设备名称</text>
                <text class="detail-value">{{ item.equipmentName }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">故障类型</text>
                <view class="detail-value">
                  <u-tag :type="getFaultTypeTagType(item.faultType)" size="small">
                    {{ item.faultType }}
                  </u-tag>
                </view>
              </view>
              <view class="detail-row">
                <text class="detail-label">根因</text>
                <text class="detail-value">{{ item.rootCause }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">停机时长</text>
                <text class="detail-value highlight">{{ item.downtime }}h</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">班组</text>
                <text class="detail-value">{{ item.team }}</text>
              </view>
              <view class="detail-row">
                <text class="detail-label">发生时间</text>
                <text class="detail-value">{{ item.occurTime }}</text>
              </view>
            </view>
            <up-divider v-if="index < tableData.length - 1"></up-divider>
          </view>
        </view>
      </view>
    </view>
  </view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import PageHeader from '@/components/PageHeader.vue'
// ç»Ÿè®¡æ¦‚览数据
const overviewData = ref({
  totalFaults: 156,
  totalDowntime: 1248.5,
  avgRepairTime: 8.0,
  faultRate: 3.2
})
// æ•…障类型统计
const faultTypeStats = ref([
  { name: '机械故障', value: 45, percent: 28.8 },
  { name: '电气故障', value: 32, percent: 20.5 },
  { name: '液压故障', value: 28, percent: 17.9 },
  { name: '气动故障', value: 25, percent: 16.0 },
  { name: '其他故障', value: 26, percent: 16.7 }
])
// æ ¹å› åˆ†æžç»Ÿè®¡
const rootCauseStats = ref([
  { name: '操作不当', value: 35, percent: 22.4 },
  { name: '设备老化', value: 28, percent: 17.9 },
  { name: '维护不足', value: 22, percent: 14.1 },
  { name: '环境因素', value: 18, percent: 11.5 },
  { name: '设计缺陷', value: 15, percent: 9.6 },
  { name: '其他原因', value: 38, percent: 24.4 }
])
// è¡¨æ ¼æ•°æ®
const tableData = ref([
  {
    equipmentName: '生产线A-01',
    faultType: '机械故障',
    rootCause: '轴承磨损',
    downtime: 12.5,
    team: '生产一班',
    occurTime: '2024-01-15 14:30'
  },
  {
    equipmentName: '检测设备B-02',
    faultType: '电气故障',
    rootCause: '电路短路',
    downtime: 8.0,
    team: '生产二班',
    occurTime: '2024-01-16 09:15'
  },
  {
    equipmentName: '辅助设备C-03',
    faultType: '液压故障',
    rootCause: '油管泄漏',
    downtime: 6.5,
    team: '维修班',
    occurTime: '2024-01-17 16:45'
  },
  {
    equipmentName: '生产线A-02',
    faultType: '气动故障',
    rootCause: '气压不足',
    downtime: 4.0,
    team: '生产一班',
    occurTime: '2024-01-18 11:20'
  },
  {
    equipmentName: '检测设备B-01',
    faultType: '机械故障',
    rootCause: '传动带断裂',
    downtime: 15.0,
    team: '生产二班',
    occurTime: '2024-01-19 08:30'
  }
])
// æ–¹æ³•
const goBack = () => {
  uni.navigateBack()
}
const getFaultTypeTagType = (faultType) => {
  const typeMap = {
    '机械故障': 'error',
    '电气故障': 'warning',
    '液压故障': 'info',
    '气动故障': 'success',
    '其他故障': 'primary'
  }
  return typeMap[faultType] || 'primary'
}
// ç”Ÿå‘½å‘¨æœŸ
onMounted(() => {
  console.log('故障分析页面已加载')
})
</script>
<style scoped>
.fault-analysis-page {
  background-color: #f5f5f5;
  min-height: 100vh;
}
.overview-section,
.stat-section,
.table-section {
  margin: 15px;
}
.section-item {
  background: #ffffff;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  overflow: hidden;
}
.item-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 15px;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.item-left {
  display: flex;
  align-items: center;
}
.document-icon {
  width: 28px;
  height: 28px;
  background: rgba(255, 255, 255, 0.2);
  border-radius: 6px;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-right: 10px;
}
.item-id {
  color: #ffffff;
  font-size: 16px;
  font-weight: 600;
}
.overview-content {
  display: flex;
  justify-content: space-around;
  padding: 20px 15px;
}
.overview-item {
  text-align: center;
  flex: 1;
}
.overview-number {
  font-size: 24px;
  font-weight: bold;
  color: #007aff;
  margin-bottom: 5px;
}
.overview-label {
  font-size: 12px;
  color: #666;
}
.item-details {
  padding: 15px;
}
.detail-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 12px 0;
  border-bottom: 1px solid #f0f0f0;
}
.detail-row:last-child {
  border-bottom: none;
}
.detail-label {
  font-size: 14px;
  color: #666;
  min-width: 80px;
}
.detail-value {
  font-size: 14px;
  color: #333;
  font-weight: 500;
  text-align: right;
}
.detail-value.highlight {
  color: #007aff;
  font-weight: bold;
}
.fault-list {
  padding: 15px;
}
.fault-item {
  margin-bottom: 15px;
}
.fault-item:last-child {
  margin-bottom: 0;
}
</style>
src/pages/equipmentManagement/repair/add.vue
@@ -356,7 +356,7 @@
        // ç¼–辑模式,获取详情
        loadForm(id);
        // å¯é€‰ï¼šèŽ·å–åŽæ¸…é™¤å­˜å‚¨çš„id,避免影响后续操作
        // uni.removeStorageSync('repairId');
        uni.removeStorageSync('repairId');
    } else {
        // æ–°å¢žæ¨¡å¼
        loadForm();
src/pages/index.vue
@@ -268,21 +268,26 @@
        label: '设备保养',
    },
    {
        icon: '/static/images/icon/shebeixunjian@2x.png',
        icon: '/static/images/icon/xunjianshangchuan@2x.png',
        label: '巡检上传',
    },
    {
        icon: 'flash',
        icon: '/static/images/icon/guzhangfenxi@2x.png',
        label: '分析追溯',
        bgColor: '#ff9800'
    },
    {
        icon: '/static/images/icon/zhinengpaidan@2x.png',
        label: '智能派单',
        bgColor: '#ff6b35'
    },
    {
        icon: 'file-text',
        icon: '/static/images/icon/zuoyezhidao@2x.png',
        label: '作业指导',
        bgColor: '#4caf50'
    },
    {
        icon: 'checkmark-circle',
        icon: '/static/images/icon/jieguoyanzheng@2x.png',
        label: '结果验证',
        bgColor: '#9c27b0'
    }
@@ -382,6 +387,11 @@
                url: '/pages/inspectionUpload/index'
            });
            break;
        case '分析追溯':
            uni.navigateTo({
                url: '/pages/equipmentManagement/faultAnalysis/index'
            });
            break;
        case '智能派单':
            uni.navigateTo({
                url: '/pages/equipmentManagement/smartDispatch/index'
src/pages/inspectionUpload/components/qrCodeFormDia.vue
@@ -42,36 +42,15 @@
          
          <u-form-item label="附件" prop="storageBlobDTO" labelWidth="80">
            <view class="upload-container">
              <u-upload
                :fileList="form.storageBlobDTO"
                @afterRead="afterRead"
                @delete="deleteFile"
                name="files"
                multiple
                :maxCount="10"
                :maxSize="50 * 1024 * 1024"
                accept="image/*,video/*"
                :previewFullImage="true"
                :camera="true"
                :gallery="true"
              ></u-upload>
              <view class="upload-actions">
                <u-button
                  type="primary"
                  size="small"
                  @click="chooseImage"
                  :customStyle="{ marginRight: '10px' }"
                >
                  æ‹ç…§
                </u-button>
                <u-button
                  type="success"
                  size="small"
                  @click="chooseVideo"
                >
                  å½•像
                </u-button>
              </view>
              <ImageUpload
                v-model="form.storageBlobDTO"
                :limit="10"
                :fileSize="50"
                :fileType="['jpg', 'jpeg', 'png', 'mp4', 'mov']"
                :maxVideoDuration="60"
                :statusType="0"
                @update:modelValue="handleStorageBlobUpdate"
              />
            </view>
          </u-form-item>
          
@@ -102,9 +81,10 @@
</template>
<script setup>
import { reactive, ref, onMounted, nextTick } from 'vue'
import { reactive, ref, onMounted, onUnmounted, nextTick } from 'vue'
import { addOrEditQrCodeRecord } from '@/api/inspectionUpload/index.js'
import useUserStore from '@/store/modules/user.ts'
import ImageUpload from '@/components/imageUpload/index.vue'
const emit = defineEmits(['closeDia'])
@@ -113,6 +93,9 @@
const userStore = useUserStore()
const userInfo = ref({})
const locationLoading = ref(false)
// è¯·æ±‚取消标志
let isRequestCancelled = false
// èŽ·å–å½“å‰æ—¶é—´
function getCurrentDateTime() {
@@ -149,125 +132,50 @@
onMounted(async () => {
  try {
    const res = await userStore.getInfo()
    // æ£€æŸ¥ç»„件是否还存在
    if (!isRequestCancelled && userInfo.value !== undefined) {
    userInfo.value = res.user
    form.scannerName = userInfo.value.nickName
    form.scannerId = userInfo.value.userId
    form.scanTime = getCurrentDateTime()
    }
  } catch (error) {
    console.error('获取用户信息失败:', error)
  }
})
// æ–‡ä»¶ä¸Šä¼ å¤„理
const afterRead = (event) => {
  const { file } = event
  console.log('文件选择:', file)
  // ç›´æŽ¥æ·»åŠ åˆ°æ–‡ä»¶åˆ—è¡¨ï¼Œä¸ä¸Šä¼ åˆ°æœåŠ¡å™¨
  const fileItem = {
    url: file.url,
    name: file.name || `文件_${Date.now()}`,
    status: 'success',
    size: file.size || 0,
    type: file.type || 'image/jpeg'
  }
  form.storageBlobDTO.push(fileItem)
  uni.showToast({
    title: '文件添加成功',
    icon: 'success'
  })
}
// æ‹ç…§
const chooseImage = () => {
  uni.chooseImage({
    count: 1,
    sizeType: ['original', 'compressed'],
    sourceType: ['camera'],
    success: (res) => {
      console.log('拍照成功:', res)
      const tempFilePath = res.tempFilePaths[0]
      const fileItem = {
        url: tempFilePath,
        name: `照片_${Date.now()}.jpg`,
        status: 'success',
        type: 'image/jpeg'
      }
      form.storageBlobDTO.push(fileItem)
      uni.showToast({
        title: '拍照成功',
        icon: 'success'
      })
    },
    fail: (err) => {
      console.error('拍照失败:', err)
      uni.showToast({
        title: '拍照失败',
        icon: 'error'
      })
    }
  })
}
// å½•像
const chooseVideo = () => {
  uni.chooseVideo({
    sourceType: ['camera'],
    maxDuration: 60, // æœ€å¤§60秒
    camera: 'back',
    success: (res) => {
      console.log('录像成功:', res)
      const tempFilePath = res.tempFilePath
      const fileItem = {
        url: tempFilePath,
        name: `视频_${Date.now()}.mp4`,
        status: 'success',
        type: 'video/mp4',
        duration: res.duration,
        size: res.size
      }
      form.storageBlobDTO.push(fileItem)
      uni.showToast({
        title: '录像成功',
        icon: 'success'
      })
    },
    fail: (err) => {
      console.error('录像失败:', err)
      uni.showToast({
        title: '录像失败',
        icon: 'error'
      })
    }
  })
}
// åˆ é™¤æ–‡ä»¶
const deleteFile = (event) => {
  const { index } = event
  form.storageBlobDTO.splice(index, 1)
// å¤„理storageBlobDTO数据更新
const handleStorageBlobUpdate = (value) => {
  form.storageBlobDTO = value || []
}
// èŽ·å–å½“å‰ä½ç½®
const getCurrentLocation = () => {
  // æ£€æŸ¥ç»„件是否还存在
  if (isRequestCancelled) return
  locationLoading.value = true
  uni.showLoading({ title: '获取位置中...' })
  
  uni.getLocation({
    type: 'gcj02',
    success: (res) => {
      // æ£€æŸ¥ç»„件是否还存在
      if (isRequestCancelled) {
        uni.hideLoading()
        return
      }
      // ä½¿ç”¨é€†åœ°ç†ç¼–码获取地址信息
      uni.request({
        url: `https://restapi.amap.com/v3/geocode/regeo?key=c120a5dc69a9f61839f7763e6057005f&location=${res.longitude},${res.latitude}&radius=1000&extensions=all`,
        success: (geoRes) => {
          // æ£€æŸ¥ç»„件是否还存在
          if (isRequestCancelled) {
            uni.hideLoading()
            return
          }
          uni.hideLoading()
          locationLoading.value = false
          
@@ -303,6 +211,12 @@
          }
        },
        fail: (err) => {
          // æ£€æŸ¥ç»„件是否还存在
          if (isRequestCancelled) {
            uni.hideLoading()
            return
          }
          uni.hideLoading()
          locationLoading.value = false
          console.error('逆地理编码失败:', err)
@@ -318,6 +232,12 @@
      })
    },
    fail: (err) => {
      // æ£€æŸ¥ç»„件是否还存在
      if (isRequestCancelled) {
        uni.hideLoading()
        return
      }
      uni.hideLoading()
      locationLoading.value = false
      uni.showToast({
@@ -335,15 +255,11 @@
// æ‰“开弹框
const openDialog = async (row) => {
  console.log('弹框接收到的数据:', row)
  console.log('弹框打开前状态:', dialogVisitable.value)
  dialogVisitable.value = true
  form.deviceName = row.deviceName || ''
  form.location = row.location || ''
  form.qrCodeId = row.qrCodeId || row.id || ''
  form.qrCodeId = row.qrCodeId
  form.storageBlobDTO = []
  console.log('弹框打开后状态:', dialogVisitable.value)
  console.log('弹框表单数据:', form)
  
  // å¼ºåˆ¶æ›´æ–°è§†å›¾
@@ -354,6 +270,9 @@
// æäº¤è¡¨å•
const submitForm = async () => {
  try {
    // æ£€æŸ¥ç»„件是否还存在
    if (isRequestCancelled) return
    console.log('开始提交表单,当前表单数据:', form)
    
    // è¡¨å•验证
@@ -389,15 +308,27 @@
      scannerName: form.scannerName,
      scannerId: form.scannerId,
      scanTime: form.scanTime,
      storageBlobDTO: form.storageBlobDTO,
      storageBlobDTO: form.storageBlobDTO.map(file => ({
        id: file.id, // æ·»åŠ id字段
        url: file.url,
        bucketFilename: file.bucketFilename || file.name,
        downloadUrl: file.downloadUrl || file.url,
        type: 0,
        size: file.size,
        createTime: file.createTime || new Date().getTime()
      })),
      qrCode: {
        id: form.qrCodeId || form.qrCode.id
        id: form.qrCodeId
      }
    }
    
    console.log('准备提交的数据:', submitData)
    
    const response = await addOrEditQrCodeRecord(submitData)
    // æ£€æŸ¥ç»„件是否还存在
    if (isRequestCancelled) return
    console.log('提交响应:', response)
    
    uni.showToast({
@@ -407,6 +338,9 @@
    
    cancel()
  } catch (error) {
    // æ£€æŸ¥ç»„件是否还存在
    if (isRequestCancelled) return
    console.error('提交失败:', error)
    
    // æ˜¾ç¤ºæ›´è¯¦ç»†çš„错误信息
@@ -430,6 +364,25 @@
  dialogVisitable.value = false
  emit('closeDia')
}
// ç»„件销毁时的清理
onUnmounted(() => {
  // è®¾ç½®å–消标志,阻止后续的异步操作
  isRequestCancelled = true
  // æ¸…理状态
  if (locationLoading.value) {
    locationLoading.value = false
  }
  // å…³é—­å¼¹çª—
  if (dialogVisitable.value) {
    dialogVisitable.value = false
  }
  // éšè—å¯èƒ½æ˜¾ç¤ºçš„加载提示
  uni.hideLoading()
})
defineExpose({ openDialog })
</script>
@@ -491,12 +444,5 @@
.upload-container {
  width: 100%;
}
.upload-actions {
  display: flex;
  justify-content: flex-start;
  margin-top: 10px;
  gap: 10px;
}
</style>
src/pages/inspectionUpload/index.vue
@@ -1,74 +1,14 @@
<template>
  <view class="inspection-upload-page">
    <!-- é¡µé¢å¤´éƒ¨ -->
    <PageHeader title="巡检上传" />
    <!-- æ ‡ç­¾é¡µ -->
    <view class="tabs-container">
      <view class="custom-tabs">
        <view
          v-for="(tab, index) in tabs"
          :key="index"
          class="tab-item"
          :class="{ 'tab-active': currentTabIndex === index }"
          @click="handleTabChange(index)"
        >
          {{ tab.name }}
        </view>
        <view class="tab-line" :style="{ left: currentTabIndex * 50 + '%' }"></view>
      </view>
    </view>
    <!-- æ‰«ç æ¨¡å— -->
    <view v-if="activeTab === 'qrCode'" class="scan-section">
      <view class="scan-controls">
        <u-button
          :type="isScanning ? 'error' : 'primary'"
          :loading="scanLoading"
          @click="toggleScan"
        >
          {{ scanButtonText }}
        </u-button>
      </view>
      <!-- æ‰«ç åŒºåŸŸ -->
      <view v-show="isScanning" class="qr-scan-container">
        <camera
          class="qr-camera"
          device-position="back"
          flash="off"
          @scancode="handleScanCode"
          @error="handleCameraError"
        ></camera>
        <view class="scan-overlay">
          <view class="scan-frame"></view>
          <view class="scan-tip">请将二维码放入框内</view>
        </view>
      </view>
      <!-- çŠ¶æ€æç¤º -->
      <view class="status-info">
        <u-alert
          v-if="cameraError"
          :title="cameraError"
          type="error"
          :showIcon="true"
          :closable="true"
          @close="cameraError = ''"
        ></u-alert>
        <view v-if="isScanning" class="scanning-text">
          <u-loading-icon mode="circle" color="#1890ff" size="20"></u-loading-icon>
          <text class="scanning-label">正在扫描二维码...</text>
        </view>
      </view>
    </view>
    <PageHeader title="巡检上传"  @back="goBack"/>
    
    <!-- æ•°æ®åˆ—表 -->
    <view class="table-section">
      <!-- ç”Ÿäº§å·¡æ£€åˆ—表 -->
      <view v-if="activeTab === 'task'" class="task-list">
      <view class="task-list">
        <view 
          v-for="(item, index) in tableData"
          v-for="(item, index) in taskTableData"
          :key="index"
          class="task-item"
          @click="handleAdd(item)"
@@ -91,147 +31,127 @@
              >
                ä¸Šä¼ 
              </u-button>
            </view>
          </view>
          <view class="task-details">
            <view class="detail-item">
              <text class="detail-label">备注:</text>
              <text class="detail-value">{{ item.remarks || '无' }}</text>
            </view>
            <view class="detail-item">
              <text class="detail-label">执行人:</text>
              <text class="detail-value">{{ item.inspector }}</text>
            </view>
          </view>
        </view>
      </view>
      <!-- çŽ°åœºå·¡æ£€åˆ—è¡¨ -->
      <view v-if="activeTab === 'qrCode'" class="qr-list">
        <view
          v-for="(item, index) in tableData"
          :key="index"
          class="qr-item"
          @click="viewFile(item)"
        >
          <view class="qr-header">
            <view class="qr-info">
              <text class="device-name">{{ item.qrCode?.deviceName }}</text>
              <text class="device-location">{{ item.qrCode?.location }}</text>
            </view>
            <view class="qr-actions">
              <u-button 
                type="primary"
                type="info"
                size="small"
                @click.stop="viewFile(item)"
                @click.stop="startScanForTask(item)"
                :customStyle="{
                  borderRadius: '15px',
                  height: '30px',
                  fontSize: '12px'
                }"
              >
                æŸ¥çœ‹é™„ä»¶
                æ‰«ç 
              </u-button>
            </view>
          </view>
          <view class="qr-details">
          <view class="task-details">
            <view class="detail-item">
              <text class="detail-label">巡检人:</text>
              <text class="detail-value">{{ item.scanner }}</text>
              <text class="detail-label">备注</text>
              <text class="detail-value">{{ item.remarks || '无' }}</text>
            </view>
            <view class="detail-item">
              <text class="detail-label">巡检时间:</text>
              <text class="detail-value">{{ item.scanTime }}</text>
              <text class="detail-label">执行人</text>
              <text class="detail-value">{{ item.inspector }}</text>
            </view>
          </view>
        </view>
      </view>
      
      <!-- ç©ºçŠ¶æ€ -->
      <view v-if="tableData.length === 0 && !tableLoading" class="empty-state">
        <u-empty
          mode="data"
          text="暂无数据"
          :iconSize="80"
        ></u-empty>
      </view>
      <!-- åŠ è½½çŠ¶æ€ -->
      <view v-if="tableLoading" class="loading-state">
        <u-loading-icon mode="circle" color="#1890ff" size="40"></u-loading-icon>
        <text class="loading-text">加载中...</text>
      <view v-if="taskTableData.length === 0" class="no-data">
        <text>暂无数据</text>
      </view>
    </view>
    
    <!-- åˆ†é¡µ -->
    <view v-if="total > 0" class="pagination-container">
      <u-pagination
        :total="total"
        :current="pageNum"
        :pageSize="pageSize"
        @change="handlePageChange"
        :showTotal="true"
        :showSizer="false"
        :showJumper="false"
      ></u-pagination>
    <!-- æ‰«ç åŒºåŸŸ - å…¨å±€å¼¹çª— -->
    <view v-if="isScanning" class="qr-scan-overlay">
      <view class="qr-scan-container">
        <view class="scan-header">
          <text class="scan-title">扫描二维码</text>
          <u-button
            type="error"
            size="small"
            @click.stop="stopScan"
            :customStyle="{
              borderRadius: '15px',
              height: '30px',
              fontSize: '12px'
            }"
          >
            å…³é—­
          </u-button>
        </view>
        <camera
          class="qr-camera"
          device-position="back"
          flash="off"
          @scancode="handleScanCode"
          @error="handleCameraError"
        ></camera>
        <view class="scan-frame-wrapper">
          <view class="scan-frame"></view>
          <view class="scan-tip">请将二维码放入框内</view>
        </view>
        <u-alert
          v-if="cameraError"
          :title="cameraError"
          type="error"
          :showIcon="true"
          :closable="true"
          @close="cameraError = ''"
          :customStyle="{
            margin: '10px 0'
          }"
        ></u-alert>
      </view>
    </view>
    
    <!-- å¼¹çª—组件 -->
    <form-dia ref="formDia" @closeDia="handleQuery"></form-dia>
    <qr-code-form-dia ref="qrCodeFormDia" @closeDia="handleQuery"></qr-code-form-dia>
  </view>
</template>
<script setup>
import { onMounted, ref, reactive, computed, nextTick } from 'vue'
import { onMounted, onUnmounted, ref, nextTick } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import PageHeader from '@/components/PageHeader.vue'
import FormDia from './components/formDia.vue'
import QrCodeFormDia from './components/qrCodeFormDia.vue'
import { qrCodeScanRecordList } from '@/api/inspectionUpload/index.js'
import { getInspectionTaskList } from '@/api/equipmentManagement/inspection.js'
import { getLedgerById } from '@/api/equipmentManagement/ledger.js'
import {inspectionTaskList} from "@/api/inspectionManagement";
// import ViewQrCodeFiles from '@/pages/inspectionManagement/components/viewQrCodeFiles.vue'
// ç»„件引用
const formDia = ref()
const qrCodeFormDia = ref()
// å½“前标签
const activeTab = ref('task')
const tabName = ref('task')
const currentTabIndex = ref(0)
// æ ‡ç­¾é¡µæ•°æ®
const tabs = reactive([
  { name: '生产巡检' },
  { name: '现场巡检' }
])
// åŠ è½½æç¤ºæ–¹æ³•
const showLoadingToast = (message) => {
  uni.showLoading({
    title: message,
    mask: true
  })
}
const closeToast = () => {
  uni.hideLoading()
}
// è¡¨æ ¼æ•°æ®
const tableData = ref([])
const tableLoading = ref(false)
const total = ref(0)
const pageNum = ref(1)
const pageSize = ref(10)
const taskTableData = ref([]) // ç”Ÿäº§å·¡æ£€æ•°æ®
// å½“前扫描的任务
const currentScanningTask = ref(null)
// è¯·æ±‚取消标志,用于取消正在进行的请求
let isRequestCancelled = false
// æ‰«ç ç›¸å…³çŠ¶æ€
const isScanning = ref(false)
const scanLoading = ref(false)
const cameraError = ref('')
// è®¡ç®—属性
const scanButtonText = computed(() => {
  if (scanLoading.value) return '正在初始化...'
  return isScanning.value ? '停止扫码' : '开始扫码'
})
// ç”Ÿå‘½å‘¨æœŸ
onMounted(() => {
  // å»¶è¿Ÿåˆå§‹åŒ–,确保DOM已渲染
  nextTick(() => {
    handleTabClick({ props: { name: 'task' } })
    getList()
  })
})
@@ -240,84 +160,112 @@
  getList()
})
// æ ‡ç­¾é¡µåˆ‡æ¢
const handleTabChange = (index) => {
  currentTabIndex.value = index
  const tabNames = ['task', 'qrCode']
  activeTab.value = tabNames[index]
  tabName.value = tabNames[index]
  tableData.value = []
  pageNum.value = 1
  getList()
}
// ç»„件销毁时的清理
onUnmounted(() => {
  // è®¾ç½®å–消标志,阻止后续的异步操作
  isRequestCancelled = true
// æ ‡ç­¾é¡µç‚¹å‡»ï¼ˆå…¼å®¹æ—§æ–¹æ³•)
const handleTabClick = (tab) => {
  tabName.value = tab.props.name
  activeTab.value = tab.props.name
  tableData.value = []
  getList()
  // åœæ­¢æ‰«ç 
  if (isScanning.value) {
    isScanning.value = false
  }
})
// è¿”回上一页
const goBack = () => {
  uni.navigateBack()
}
// æŸ¥è¯¢æ•°æ®
const handleQuery = () => {
  pageNum.value = 1
  pageSize.value = 10
  getList()
}
// èŽ·å–åˆ—è¡¨æ•°æ®
const getList = () => {
  tableLoading.value = true
  if (tabName.value === "task") {
    inspectionTaskList({size: pageSize.value, current: pageNum.value}).then(res => {
      tableLoading.value = false;
      tableData.value = res.data.records;
      total.value = res.data.total;
    })
  } else {
    qrCodeScanRecordList({size: pageSize.value, current: pageNum.value}).then(res => {
      tableLoading.value = false;
      tableData.value = res.data.records;
      total.value = res.data.total;
    })
  // æ˜¾ç¤ºåŠ è½½æç¤º
  showLoadingToast('加载中...')
  // è®¾ç½®å–消标志
  isRequestCancelled = false
  inspectionTaskList({}).then(res => {
    // æ£€æŸ¥ç»„件是否还存在且请求未被取消
    if (!isRequestCancelled) {
      console.log('生产巡检API返回数据:', res);
      // å¤„理不同的数据结构
      let records = [];
      if (res && res.data) {
        // å°è¯•多种可能的数据结构
        if (Array.isArray(res.data.records)) {
          records = res.data.records;
        } else if (Array.isArray(res.data.rows)) {
          records = res.data.rows;
        } else if (Array.isArray(res.data)) {
          records = res.data;
        } else if (Array.isArray(res.data.list)) {
          records = res.data.list;
  }
}
// åˆ†é¡µå˜åŒ–
const handlePageChange = (page) => {
  pageNum.value = page
  getList()
      if (records.length > 0) {
        taskTableData.value = records;
        console.log('生产巡检数据设置成功,记录数:', records.length);
      } else {
        console.warn('生产巡检数据为空或格式不正确:', res);
        taskTableData.value = [];
        uni.showToast({
          title: '暂无巡检任务数据',
          icon: 'none'
        });
}
    }
    // å…³é—­åŠ è½½æç¤º
    closeToast()
  }).catch(err => {
    // æ£€æŸ¥ç»„件是否还存在且请求未被取消
    if (!isRequestCancelled) {
      console.error('获取生产巡检数据失败:', err);
      taskTableData.value = [];
      // æ·»åŠ é”™è¯¯æç¤º
      uni.showToast({
        title: '获取数据失败',
        icon: 'error'
      })
    }
    // å…³é—­åŠ è½½æç¤º
    closeToast()
  })
}
// ä¸Šä¼ 
const handleAdd = (row) => {
  nextTick(() => {
    formDia.value?.openDialog(row)
  })
}
// æŸ¥çœ‹é™„ä»¶
const viewFile = (row) => {
  console.log('查看附件:', row)
  uni.showToast({
    title: '查看附件功能开发中',
    icon: 'none'
  })
}
// æ‰«ç ç›¸å…³æ–¹æ³•
const toggleScan = async () => {
  if (isScanning.value) {
    await stopScan()
    // æ£€æŸ¥ç»„件是否还存在
    if (formDia.value && formDia.value.openDialog) {
      formDia.value.openDialog(row)
  } else {
    await startScan()
      console.error('上传组件引用不存在')
      uni.showToast({
        title: '组件未准备好',
        icon: 'error'
      })
  }
  })
}
const startScan = async () => {
// ä¸ºæŒ‡å®šä»»åŠ¡å¼€å§‹æ‰«ç 
const startScanForTask = async (task) => {
  try {
    scanLoading.value = true
    // è®°å½•当前扫描的任务
    currentScanningTask.value = task
    console.log('为任务开始扫码:', task.taskName)
    // æ˜¾ç¤ºæ‰«æç•Œé¢
    isScanning.value = true
    // ä½¿ç”¨uniapp的扫码API
    uni.scanCode({
      success: (res) => {
@@ -330,29 +278,37 @@
          title: '扫码失败',
          icon: 'error'
        })
        // å…³é—­æ‰«æç•Œé¢
        isScanning.value = false
      },
      complete: () => {
        scanLoading.value = false
        // æ‰«ç å®ŒæˆåŽå…³é—­æ‰«æç•Œé¢
        setTimeout(() => {
          isScanning.value = false
        }, 1000)
      }
    })
  } catch (e) {
    console.error('启动扫码失败:', e)
    scanLoading.value = false
    uni.showToast({
      title: '启动扫码失败',
      icon: 'error'
    })
    isScanning.value = false
  }
}
const stopScan = async () => {
// åœæ­¢æ‰«ç 
const stopScan = () => {
  isScanning.value = false
  currentScanningTask.value = null
}
// æ‰«ç æˆåŠŸå¤„ç†
const handleScanSuccess = async (result) => {
  try {
    console.log('处理扫码结果:', result)
    console.log('当前关联任务:', currentScanningTask.value?.taskName)
    
    uni.showToast({
      title: '识别成功',
@@ -361,17 +317,51 @@
    
    // è§£æžäºŒç»´ç æ•°æ®
    let qrData
    let deviceId = ''
    try {
      qrData = JSON.parse(result.result)
      console.log('解析的二维码数据:', qrData)
      deviceId = qrData.deviceId || qrData.qrCodeId
    } catch (e) {
      // å¦‚果不是JSON格式,直接使用扫码结果作为设备名称
      qrData = {
        deviceName: result.result,
        location: '',
        qrCodeId: result.result // æ·»åŠ äºŒç»´ç ID
      // å¦‚果不是JSON格式,尝试从URL中提取deviceId
      const url = result.result
      if (url.includes('deviceId=')) {
        // ä»ŽURL中提取deviceId
        const match = url.match(/deviceId=(\d+)/)
        if (match && match[1]) {
          deviceId = match[1]
      }
      console.log('使用默认数据格式:', qrData)
      }
      qrData = {
        deviceName: deviceId ? `设备${deviceId}` : result.result,
        location: '',
        qrCodeId: deviceId // ä½¿ç”¨æå–çš„deviceId或原始结果
      }
    }
    // å¦‚果有设备ID,尝试从API获取真实的设备名称
    if (deviceId) {
      try {
        console.log('正在查询设备信息,设备ID:', deviceId)
        const response = await getLedgerById(deviceId)
        console.log('设备信息查询结果:', response)
        if (response.code === 200 && response.data) {
          qrData.deviceName = response.data.deviceName || `设备${deviceId}`
          qrData.location = response.data.storageLocation || ''
          console.log('获取到设备名称:', qrData.deviceName)
        } else {
          console.warn('设备信息查询失败,使用默认名称')
          qrData.deviceName = qrData.deviceName || `设备${deviceId}`
        }
      } catch (apiError) {
        console.error('查询设备信息失败:', apiError)
        // API调用失败时使用默认名称
        qrData.deviceName = qrData.deviceName || `设备${deviceId}`
      }
    }
    
    // ç¡®ä¿æ•°æ®å®Œæ•´æ€§
@@ -379,51 +369,40 @@
      qrData.deviceName = result.result
    }
    if (!qrData.qrCodeId) {
      qrData.qrCodeId = result.result
      qrData.qrCodeId = deviceId || result.result
    }
    
    callBackendAPI(qrData)
    // å°†æ‰«ç æ•°æ®ä¸Žä»»åŠ¡å…³è”
    if (currentScanningTask.value) {
      // å…³è”任务信息
      const taskData = {
        ...currentScanningTask.value,
        qrCodeData: qrData
      }
      // æ‰“开上传弹窗,传递关联后的任务数据
      nextTick(() => {
        if (formDia.value && formDia.value.openDialog) {
          formDia.value.openDialog(taskData)
        } else {
          console.error('上传组件引用不存在')
          uni.showToast({
            title: '组件未准备好',
            icon: 'error'
          })
        }
      })
    }
  } catch (error) {
    console.error('处理扫码结果失败:', error)
    uni.showToast({
      title: error.message || '数据解析失败',
      icon: 'error'
    })
  } finally {
    // å…³é—­æ‰«æç•Œé¢
    isScanning.value = false
  }
}
const callBackendAPI = (result) => {
  console.log('准备打开弹框,数据:', result)
  console.log('弹框组件引用:', qrCodeFormDia.value)
  // ç¡®ä¿ç»„件引用存在
  if (qrCodeFormDia.value) {
    console.log('直接调用弹框openDialog方法')
    qrCodeFormDia.value.openDialog(result)
  } else {
    // å¦‚果组件引用不存在,等待下一个tick
    console.log('组件引用不存在,等待nextTick')
    nextTick(() => {
      console.log('nextTick后弹框组件引用:', qrCodeFormDia.value)
      if (qrCodeFormDia.value) {
        console.log('nextTick后调用弹框openDialog方法')
        qrCodeFormDia.value.openDialog(result)
      } else {
        console.error('弹框组件引用不存在')
        uni.showToast({
          title: '弹框组件未准备好',
          icon: 'error'
        })
      }
    })
  }
}
// æ‰«ç å¤„理
const handleScanCode = (result) => {
  console.log('扫码结果:', result)
  handleScanSuccess(result)
}
// æ‘„像头错误处理
@@ -434,104 +413,192 @@
</script>
<style scoped lang="scss">
// å¯¼å…¥é”€å”®æ¨¡å—公共样式
@import '@/styles/sales-common.scss';
// é¡µé¢å®¹å™¨æ ·å¼
.inspection-upload-page {
  min-height: 100vh;
  background-color: #f5f5f5;
}
.tabs-container {
  background-color: #fff;
  margin: 0;
  border-bottom: 1px solid #e8e8e8;
}
.custom-tabs {
  display: flex;
  background: #f8f9fa;
  position: relative;
  background-color: #fff;
  width: 100%;
}
.tab-item {
// åˆ—表容器样式
.table-section {
  padding: 20px;
}
// ä»»åŠ¡åˆ—è¡¨æ ·å¼ - ä½¿ç”¨é”€å”®å°è´¦çš„æ ·å¼è§„范
.task-list {
  .task-item {
    background: #ffffff;
    border-radius: 12px;
    margin-bottom: 16px;
    overflow: hidden;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
    padding: 0 16px;
    &:active {
      transform: scale(0.98);
      box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
    }
  }
}
// é¡¹ç›®å¤´éƒ¨æ ·å¼
.task-header {
  padding: 16px 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 0;
}
.task-info {
  flex: 1;
  text-align: center;
  padding: 20px 0;
  font-size: 16px;
}
.task-name {
  font-size: 14px;
  color: #333;
  font-weight: 500;
  color: #606266;
  transition: all 0.3s ease;
  cursor: pointer;
  position: relative;
  z-index: 2;
  margin-bottom: 0;
  line-height: 1.4;
}
.tab-item.tab-active {
  color: #1890ff;
  font-weight: 600;
.task-location {
  font-size: 12px;
  color: #666;
  margin-top: 4px;
}
.tab-line {
  position: absolute;
  bottom: 0;
  width: 50%;
  height: 3px;
  background-color: #1890ff;
  transition: left 0.3s ease;
}
.scan-section {
  background-color: #fff;
    padding: 10px;
}
.scan-controls {
// ä»»åŠ¡æ“ä½œæŒ‰é’®æ ·å¼
.task-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-left: 0;
}
// ä»»åŠ¡è¯¦æƒ…æ ·å¼ - ä½¿ç”¨é”€å”®å°è´¦çš„详情行样式
.task-details {
  padding: 16px 0;
  .detail-item {
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    margin-bottom: 8px;
    &:last-child {
      margin-bottom: 0;
    }
    .detail-label {
      font-size: 12px;
      color: #777777;
      min-width: 60px;
      flex-shrink: 0;
    }
    .detail-value {
      font-size: 12px;
      color: #000000;
      text-align: right;
      flex: 1;
      margin-left: 16px;
      line-height: 1.4;
    }
  }
}
// æ— æ•°æ®æç¤ºæ ·å¼ - ä½¿ç”¨é”€å”®å°è´¦çš„æ ·å¼
.no-data {
  padding: 40px 0;
  text-align: center;
  color: #999;
  background: none;
  margin: 0;
}
.no-data text {
  font-size: 14px;
  color: #999;
}
/* æ‰«ç å¼¹çª—样式 */
.qr-scan-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0, 0, 0, 0.8);
  z-index: 9999;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  padding: 20px;
}
.qr-scan-container {
  position: relative;
  width: 100%;
  max-width: 500px;
  margin: 0 auto;
  background: #000;
  border-radius: 8px;
  max-width: 400px;
  background-color: #000;
  border-radius: 12px;
  overflow: hidden;
}
.scan-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 15px;
  background-color: rgba(0, 0, 0, 0.7);
}
.scan-title {
  font-size: 18px;
  font-weight: 600;
  color: #fff;
}
.qr-camera {
  width: 100%;
  height: 400px;
}
.scan-frame-wrapper {
  position: relative;
  width: 100%;
  height: 300px;
}
.scan-overlay {
.scan-frame {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 70%;
  height: 70%;
  width: 80%;
  height: 80%;
  border: 3px solid #1890ff;
  border-radius: 8px;
  box-shadow: 0 0 20px rgba(24, 144, 255, 0.3);
  animation: pulse 2s infinite;
}
.scan-frame {
  width: 100%;
  height: 100%;
  border: 2px solid #fff;
  border-radius: 4px;
}
.scan-tip {
  position: absolute;
  bottom: -30px;
  bottom: 10px;
  left: 50%;
  transform: translateX(-50%);
  color: #fff;
  font-size: 14px;
  text-align: center;
  background-color: rgba(0, 0, 0, 0.6);
  padding: 5px 15px;
  border-radius: 20px;
}
@keyframes pulse {
@@ -540,104 +607,4 @@
  100% { opacity: 0.8; }
}
.status-info {
  margin-top: 16px;
  text-align: center;
}
.scanning-text {
  display: flex;
  align-items: center;
  justify-content: center;
  color: #1890ff;
  margin-top: 8px;
}
.scanning-label {
  margin-left: 8px;
  font-size: 14px;
}
.table-section {
  padding: 0 15px;
}
.task-list, .qr-list {
  .task-item, .qr-item {
    background-color: #fff;
    border-radius: 8px;
    margin-bottom: 10px;
    padding: 15px;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  }
}
.task-header, .qr-header {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  margin-bottom: 10px;
}
.task-info, .qr-info {
  flex: 1;
}
.task-name, .device-name {
  font-size: 16px;
  font-weight: 600;
  color: #333;
  margin-bottom: 4px;
}
.task-location, .device-location {
  font-size: 14px;
  color: #666;
}
.task-actions, .qr-actions {
  margin-left: 10px;
}
.task-details, .qr-details {
  .detail-item {
    display: flex;
    margin-bottom: 6px;
    .detail-label {
      font-size: 14px;
      color: #666;
      min-width: 60px;
    }
    .detail-value {
      font-size: 14px;
      color: #333;
      flex: 1;
    }
  }
}
.empty-state, .loading-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 40px 20px;
  background-color: #fff;
  border-radius: 8px;
  margin: 10px 15px;
}
.loading-text {
  margin-top: 10px;
  font-size: 14px;
  color: #666;
}
.pagination-container {
  padding: 20px 15px;
  background-color: #fff;
  margin-top: 10px;
}
</style>
src/static/images/icon/guzhangfenxi@2x.png
src/static/images/icon/jieguoyanzheng@2x.png
src/static/images/icon/xunjianshangchuan@2x.png
src/static/images/icon/zhinengpaidan@2x.png
src/static/images/icon/zuoyezhidao@2x.png