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