<template> 
 | 
  <view class="attachment-container"> 
 | 
    <!-- 头部操作区 --> 
 | 
    <view class="header-actions"> 
 | 
      <wd-button 
 | 
        icon="file-add" 
 | 
        :round="false" 
 | 
        size="small" 
 | 
        custom-class="add_btn" 
 | 
        @click="addAttachment" 
 | 
      > 
 | 
        新增 
 | 
      </wd-button> 
 | 
    </view> 
 | 
  
 | 
    <!-- 附件列表 --> 
 | 
    <view class="attachment-list"> 
 | 
      <wd-status-tip v-if="attachmentList.length === 0" image="content" tip="暂无附件" /> 
 | 
  
 | 
      <view v-for="item in attachmentList" :key="item.id" class="attachment-card"> 
 | 
        <view class="media-wrapper" @click="previewAttachment(item)"> 
 | 
          <!-- 图片预览 --> 
 | 
          <template v-if="isImageType(item.url)"> 
 | 
            <image 
 | 
              v-if="!item.loadError" 
 | 
              :src="getFullUrl(item.url)" 
 | 
              mode="aspectFill" 
 | 
              class="media-preview" 
 | 
              @error="onImageError(item)" 
 | 
              @load="onImageLoad(item)" 
 | 
            /> 
 | 
            <!-- 图片加载失败显示默认图标 --> 
 | 
            <view v-else class="file-icon-wrapper"> 
 | 
              <wd-icon name="picture" size="48px" color="#ccc" /> 
 | 
              <text class="file-name error-text">加载失败</text> 
 | 
            </view> 
 | 
          </template> 
 | 
  
 | 
          <!-- 视频预览 --> 
 | 
          <template v-else-if="isVideoType(item.url)"> 
 | 
            <video 
 | 
              v-if="!item.loadError" 
 | 
              :src="getFullUrl(item.url)" 
 | 
              class="media-preview" 
 | 
              :controls="false" 
 | 
              :show-center-play-btn="false" 
 | 
              @error="onVideoError(item)" 
 | 
            /> 
 | 
            <!-- 视频加载失败显示默认图标 --> 
 | 
            <view v-else class="file-icon-wrapper"> 
 | 
              <wd-icon name="video" size="48px" color="#ccc" /> 
 | 
              <text class="file-name error-text">加载失败</text> 
 | 
            </view> 
 | 
          </template> 
 | 
  
 | 
          <!-- 其他文件类型显示图标 --> 
 | 
          <view v-else class="file-icon-wrapper"> 
 | 
            <wd-icon name="file-outline" size="48px" color="#999" /> 
 | 
            <text class="file-name">文件</text> 
 | 
          </view> 
 | 
  
 | 
          <!-- 删除按钮 --> 
 | 
          <view class="delete-btn" @click.stop="deleteAttachment(item.id)"> 
 | 
            <wd-icon name="delete" color="#fff" size="20px" /> 
 | 
          </view> 
 | 
        </view> 
 | 
      </view> 
 | 
    </view> 
 | 
  
 | 
    <wd-toast /> 
 | 
  </view> 
 | 
</template> 
 | 
  
 | 
<script setup lang="ts"> 
 | 
import { ref, onMounted } from "vue"; 
 | 
import { useToast } from "wot-design-uni"; 
 | 
import AttachmentAPI from "@/api/product/attachment"; 
 | 
  
 | 
// H5 使用 VITE_APP_BASE_API 作为代理路径,其他平台使用 VITE_APP_API_URL 作为请求路径 
 | 
let baseUrl = import.meta.env.VITE_APP_API_URL; 
 | 
// #ifdef H5 
 | 
baseUrl = import.meta.env.VITE_APP_BASE_API; 
 | 
// #endif 
 | 
  
 | 
const toast = useToast(); 
 | 
  
 | 
// 页面参数 
 | 
const reportId = ref(""); 
 | 
const reportType = ref("绞线"); 
 | 
const attachmentList = ref<any[]>([]); 
 | 
  
 | 
const detailData = ref<any>({}); 
 | 
  
 | 
// 获取完整的图片/视频 URL 
 | 
const getFullUrl = (url: string) => { 
 | 
  if (!url) return ""; 
 | 
  // 如果已经是完整的 URL(http 或 https 开头),直接返回 
 | 
  if (url.startsWith("http://") || url.startsWith("https://")) { 
 | 
    return url; 
 | 
  } 
 | 
  // 如果是相对路径,拼接基础 URL 
 | 
  return `${baseUrl}${url.startsWith("/") ? "" : "/"}${url}`; 
 | 
}; 
 | 
  
 | 
// 从 URL 或文件名中提取扩展名 
 | 
const getExtension = (urlOrFileName: string) => { 
 | 
  if (!urlOrFileName) return ""; 
 | 
  // 移除查询参数和哈希 
 | 
  const cleanUrl = urlOrFileName.split("?")[0].split("#")[0]; 
 | 
  // 获取最后一个点后面的内容 
 | 
  const extension = cleanUrl.split(".").pop()?.toLowerCase(); 
 | 
  return extension || ""; 
 | 
}; 
 | 
  
 | 
// 判断是否为图片类型 
 | 
const isImageType = (urlOrFileName: string) => { 
 | 
  const extension = getExtension(urlOrFileName); 
 | 
  return ["jpg", "jpeg", "png", "gif", "bmp", "webp"].includes(extension); 
 | 
}; 
 | 
  
 | 
// 判断是否为视频类型 
 | 
const isVideoType = (urlOrFileName: string) => { 
 | 
  const extension = getExtension(urlOrFileName); 
 | 
  return ["mp4", "mov", "avi", "wmv", "flv", "mkv", "webm"].includes(extension); 
 | 
}; 
 | 
  
 | 
// 图片加载成功 
 | 
const onImageLoad = (item: any) => { 
 | 
  item.loadError = false; 
 | 
}; 
 | 
  
 | 
// 图片加载失败 
 | 
const onImageError = (item: any) => { 
 | 
  console.error("图片加载失败:", item.url); 
 | 
  item.loadError = true; 
 | 
}; 
 | 
  
 | 
// 视频加载失败 
 | 
const onVideoError = (item: any) => { 
 | 
  console.error("视频加载失败:", item.url); 
 | 
  item.loadError = true; 
 | 
}; 
 | 
  
 | 
// 获取附件列表 
 | 
const getAttachmentList = async (data: any) => { 
 | 
  try { 
 | 
    detailData.value = data; 
 | 
    console.log(" detailData.value", detailData.value); 
 | 
    const pages = getCurrentPages(); 
 | 
    const currentPage = pages[pages.length - 1]; 
 | 
    const options = (currentPage as any).options; 
 | 
    const currentReportId = detailData.value.id; 
 | 
  
 | 
    if (currentReportId) { 
 | 
      reportId.value = currentReportId; 
 | 
  
 | 
      // 直接调用通用查看接口查询附件列表 
 | 
      // 使用示例中的附件ID数组 [850,851] 
 | 
  
 | 
      const attachmentIds: number[] = 
 | 
        detailData.value.attachmentId !== null ? detailData.value.attachmentId.split(",") : []; // 使用HTTP文件中的示例数据 
 | 
      if (attachmentIds.length === 0) { 
 | 
        return; 
 | 
      } 
 | 
      const { data } = await AttachmentAPI.listAttachmentFiles(attachmentIds); 
 | 
      attachmentList.value = data || []; 
 | 
    } else { 
 | 
      attachmentList.value = []; 
 | 
    } 
 | 
  } catch (error) { 
 | 
    console.error("获取附件列表失败:", error); 
 | 
    toast.show("获取附件列表失败"); 
 | 
    attachmentList.value = []; 
 | 
  } 
 | 
}; 
 | 
  
 | 
// 新增附件 
 | 
const addAttachment = () => { 
 | 
  // 显示选择文件类型的弹窗 
 | 
  uni.showActionSheet({ 
 | 
    itemList: ["选择图片", "选择视频", "拍照", "录像"], 
 | 
    success: (res) => { 
 | 
      switch (res.tapIndex) { 
 | 
        case 0: // 选择图片 
 | 
          chooseImages(); 
 | 
          break; 
 | 
        case 1: // 选择视频 
 | 
          chooseVideos(); 
 | 
          break; 
 | 
        case 2: // 拍照 
 | 
          takePhoto(); 
 | 
          break; 
 | 
        case 3: // 录像 
 | 
          recordVideo(); 
 | 
          break; 
 | 
      } 
 | 
    }, 
 | 
    fail: (error) => { 
 | 
      console.error("选择文件类型失败:", error); 
 | 
      toast.show("选择文件类型失败"); 
 | 
    }, 
 | 
  }); 
 | 
}; 
 | 
  
 | 
// 选择图片 
 | 
const chooseImages = () => { 
 | 
  uni.chooseImage({ 
 | 
    count: 9, 
 | 
    sizeType: ["original", "compressed"], 
 | 
    sourceType: ["album"], 
 | 
    success: async (res) => { 
 | 
      const filePaths = Array.isArray(res.tempFilePaths) ? res.tempFilePaths : [res.tempFilePaths]; 
 | 
      await handleFileUpload(filePaths); 
 | 
    }, 
 | 
    fail: (error) => { 
 | 
      console.error("选择图片失败:", error); 
 | 
      toast.show("选择图片失败"); 
 | 
    }, 
 | 
  }); 
 | 
}; 
 | 
  
 | 
// 选择视频 
 | 
const chooseVideos = () => { 
 | 
  uni.chooseVideo({ 
 | 
    sourceType: ["album"], 
 | 
    maxDuration: 60, 
 | 
    camera: "back", 
 | 
    success: async (res) => { 
 | 
      await handleFileUpload([res.tempFilePath]); 
 | 
    }, 
 | 
    fail: (error) => { 
 | 
      console.error("选择视频失败:", error); 
 | 
      toast.show("选择视频失败"); 
 | 
    }, 
 | 
  }); 
 | 
}; 
 | 
  
 | 
// 拍照 
 | 
const takePhoto = () => { 
 | 
  uni.chooseImage({ 
 | 
    count: 1, 
 | 
    sizeType: ["original", "compressed"], 
 | 
    sourceType: ["camera"], 
 | 
    success: async (res) => { 
 | 
      const filePaths = Array.isArray(res.tempFilePaths) ? res.tempFilePaths : [res.tempFilePaths]; 
 | 
      await handleFileUpload(filePaths); 
 | 
    }, 
 | 
    fail: (error) => { 
 | 
      console.error("拍照失败:", error); 
 | 
      toast.show("拍照失败"); 
 | 
    }, 
 | 
  }); 
 | 
}; 
 | 
  
 | 
// 录像 
 | 
const recordVideo = () => { 
 | 
  uni.chooseVideo({ 
 | 
    sourceType: ["camera"], 
 | 
    maxDuration: 60, 
 | 
    camera: "back", 
 | 
    success: async (res) => { 
 | 
      await handleFileUpload([res.tempFilePath]); 
 | 
    }, 
 | 
    fail: (error) => { 
 | 
      console.error("录像失败:", error); 
 | 
      toast.show("录像失败"); 
 | 
    }, 
 | 
  }); 
 | 
}; 
 | 
  
 | 
// 处理文件上传 
 | 
const handleFileUpload = async (filePaths: string[]) => { 
 | 
  try { 
 | 
    toast.show("正在上传..."); 
 | 
  
 | 
    // 上传文件 
 | 
    const uploadResults: any = await AttachmentAPI.uploadAttachmentFiles(filePaths); 
 | 
    const result = uploadResults.map((it: any) => { 
 | 
      return it.data; 
 | 
    }); 
 | 
    console.log("result", result); 
 | 
  
 | 
    // 更新附件列表 
 | 
    const flattenedResult = result.flat(); 
 | 
    attachmentList.value.push(...flattenedResult); 
 | 
    console.log(attachmentList.value); 
 | 
  
 | 
    // 提取附件ID 
 | 
    const attachmentId = attachmentList.value.map((item: any) => item.id).join(","); 
 | 
  
 | 
    // 关联到报工 
 | 
    if (attachmentId) { 
 | 
      await AttachmentAPI.addOutputAttachments({ 
 | 
        id: parseInt(detailData.value.id), 
 | 
        attachmentId: attachmentId, 
 | 
      }); 
 | 
      detailData.value.attachmentId = attachmentId; 
 | 
    } 
 | 
  
 | 
    toast.show("上传成功"); 
 | 
  } catch (error) { 
 | 
    console.error("上传失败:", error); 
 | 
    toast.show("上传失败"); 
 | 
  } 
 | 
}; 
 | 
  
 | 
// 删除附件 
 | 
const deleteAttachment = async (aid: number) => { 
 | 
  try { 
 | 
    uni.showModal({ 
 | 
      title: "确认删除", 
 | 
      content: "确定要删除这个附件吗?", 
 | 
      success: async (res) => { 
 | 
        if (res.confirm) { 
 | 
          // 前端手动删除:直接从列表中移除这条数据 
 | 
          attachmentList.value = attachmentList.value.filter((item) => item.id !== aid); 
 | 
  
 | 
          // 获取剩余的附件ID组合 
 | 
          const attachmentId = attachmentList.value.map((item) => item.id).join(","); 
 | 
  
 | 
          // 调用报工添加附件接口,更新附件关联 
 | 
          await AttachmentAPI.addOutputAttachments({ 
 | 
            id: parseInt(detailData.value.id), 
 | 
            attachmentId: attachmentId, 
 | 
          }); 
 | 
          detailData.value.attachmentId = attachmentId; 
 | 
          toast.show("删除成功"); 
 | 
        } 
 | 
      }, 
 | 
    }); 
 | 
  } catch (error) { 
 | 
    console.error("删除失败:", error); 
 | 
    toast.show("删除失败"); 
 | 
  } 
 | 
}; 
 | 
  
 | 
// 预览附件 
 | 
const previewAttachment = (item: any) => { 
 | 
  // 根据文件类型进行预览 
 | 
  const fileType = getFileType(item.url); 
 | 
  const fullUrl = getFullUrl(item.url); 
 | 
  
 | 
  if (fileType.startsWith("image")) { 
 | 
    // 图片预览 
 | 
    uni.previewImage({ 
 | 
      urls: [fullUrl], 
 | 
      current: fullUrl, 
 | 
    }); 
 | 
  } else { 
 | 
    // 其他文件类型,可以下载或打开 
 | 
    uni.downloadFile({ 
 | 
      url: fullUrl, 
 | 
      success: (res) => { 
 | 
        uni.openDocument({ 
 | 
          filePath: res.tempFilePath, 
 | 
          success: () => { 
 | 
            // 打开文档成功 
 | 
          }, 
 | 
          fail: (error) => { 
 | 
            console.error("打开文档失败:", error); 
 | 
            toast.show("无法预览此文件类型"); 
 | 
          }, 
 | 
        }); 
 | 
      }, 
 | 
      fail: (error) => { 
 | 
        console.error("下载文件失败:", error); 
 | 
        toast.show("下载文件失败"); 
 | 
      }, 
 | 
    }); 
 | 
  } 
 | 
}; 
 | 
  
 | 
// 获取文件类型 
 | 
const getFileType = (urlOrFileName: string) => { 
 | 
  if (!urlOrFileName) return "unknown"; 
 | 
  const extension = getExtension(urlOrFileName); 
 | 
  switch (extension) { 
 | 
    case "jpg": 
 | 
    case "jpeg": 
 | 
    case "png": 
 | 
    case "gif": 
 | 
    case "bmp": 
 | 
    case "webp": 
 | 
      return "image"; 
 | 
    case "mp4": 
 | 
    case "mov": 
 | 
    case "avi": 
 | 
    case "wmv": 
 | 
    case "flv": 
 | 
    case "mkv": 
 | 
    case "webm": 
 | 
      return "video"; 
 | 
    case "pdf": 
 | 
      return "pdf"; 
 | 
    case "doc": 
 | 
    case "docx": 
 | 
      return "word"; 
 | 
    case "xls": 
 | 
    case "xlsx": 
 | 
      return "excel"; 
 | 
    case "ppt": 
 | 
    case "pptx": 
 | 
      return "powerpoint"; 
 | 
    case "txt": 
 | 
      return "text"; 
 | 
    case "zip": 
 | 
    case "rar": 
 | 
      return "archive"; 
 | 
    default: 
 | 
      return "file"; 
 | 
  } 
 | 
}; 
 | 
  
 | 
// 格式化文件大小 
 | 
const formatFileSize = (size: number) => { 
 | 
  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 = (time: string) => { 
 | 
  const date = new Date(time); 
 | 
  return date.toLocaleString(); 
 | 
}; 
 | 
  
 | 
onMounted(() => { 
 | 
  uni.$on("detailData", (data) => { 
 | 
    // 处理接收到的数据 
 | 
    getAttachmentList(data); 
 | 
  }); 
 | 
}); 
 | 
</script> 
 | 
  
 | 
<style lang="scss" scoped> 
 | 
.attachment-container { 
 | 
  padding: 12px; 
 | 
  background: #f3f9f8; 
 | 
  min-height: 100vh; 
 | 
} 
 | 
  
 | 
.header-actions { 
 | 
  margin-bottom: 12px; 
 | 
  
 | 
  :deep(.add_btn) { 
 | 
    background: #0d867f; 
 | 
    color: white; 
 | 
    border: none; 
 | 
  } 
 | 
} 
 | 
  
 | 
.attachment-list { 
 | 
  display: grid; 
 | 
  grid-template-columns: repeat(3, 1fr); 
 | 
  gap: 8px; 
 | 
  
 | 
  .attachment-card { 
 | 
    width: 100%; 
 | 
    aspect-ratio: 1; 
 | 
  } 
 | 
} 
 | 
  
 | 
.media-wrapper { 
 | 
  position: relative; 
 | 
  width: 100%; 
 | 
  height: 100%; 
 | 
  border-radius: 8px; 
 | 
  overflow: hidden; 
 | 
  background: #f5f5f5; 
 | 
  
 | 
  .media-preview { 
 | 
    width: 100%; 
 | 
    height: 100%; 
 | 
    object-fit: cover; 
 | 
  } 
 | 
  
 | 
  .file-icon-wrapper { 
 | 
    display: flex; 
 | 
    flex-direction: column; 
 | 
    align-items: center; 
 | 
    justify-content: center; 
 | 
    width: 100%; 
 | 
    height: 100%; 
 | 
    padding: 8px; 
 | 
    text-align: center; 
 | 
  
 | 
    .file-name { 
 | 
      margin-top: 8px; 
 | 
      font-size: 12px; 
 | 
      color: #666; 
 | 
      word-break: break-all; 
 | 
      display: -webkit-box; 
 | 
      -webkit-line-clamp: 2; 
 | 
      line-clamp: 2; 
 | 
      -webkit-box-orient: vertical; 
 | 
      overflow: hidden; 
 | 
  
 | 
      &.error-text { 
 | 
        color: #ff4757; 
 | 
      } 
 | 
    } 
 | 
  } 
 | 
  
 | 
  .delete-btn { 
 | 
    position: absolute; 
 | 
    top: 4px; 
 | 
    right: 4px; 
 | 
    width: 28px; 
 | 
    height: 28px; 
 | 
    border-radius: 50%; 
 | 
    background: rgba(0, 0, 0, 0.5); 
 | 
    display: flex; 
 | 
    align-items: center; 
 | 
    justify-content: center; 
 | 
    z-index: 10; 
 | 
  } 
 | 
} 
 | 
</style> 
 |