|  |  | 
 |  |  |     <view class="attachment-list"> | 
 |  |  |       <wd-status-tip v-if="attachmentList.length === 0" image="content" tip="暂无附件" /> | 
 |  |  |  | 
 |  |  |       <wd-card | 
 |  |  |         v-for="item in attachmentList" | 
 |  |  |         :key="item.id" | 
 |  |  |         type="rectangle" | 
 |  |  |         custom-class="attachment-card" | 
 |  |  |         :border="false" | 
 |  |  |       > | 
 |  |  |         <view class="attachment-item" @click="previewAttachment(item)"> | 
 |  |  |           <view class="attachment-info"> | 
 |  |  |             <view class="attachment-name">{{ item.bucketFileName || item.name }}</view> | 
 |  |  |             <view class="attachment-meta"> | 
 |  |  |               <text class="file-type">{{ getFileType(item.bucketFileName) }}</text> | 
 |  |  |               <text class="upload-time">{{ formatTime(item.createTime) }}</text> | 
 |  |  |       <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="attachment-actions" @click.stop> | 
 |  |  |             <wd-icon name="delete" color="#ff4757" @click="deleteAttachment(item.id)" /> | 
 |  |  |  | 
 |  |  |           <!-- 删除按钮 --> | 
 |  |  |           <view class="delete-btn" @click.stop="deleteAttachment(item.id)"> | 
 |  |  |             <wd-icon name="delete" color="#fff" size="20px" /> | 
 |  |  |           </view> | 
 |  |  |         </view> | 
 |  |  |       </wd-card> | 
 |  |  |       </view> | 
 |  |  |     </view> | 
 |  |  |  | 
 |  |  |     <wd-toast /> | 
 |  |  | 
 |  |  | 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 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 { | 
 |  |  | 
 |  |  | // 预览附件 | 
 |  |  | const previewAttachment = (item: any) => { | 
 |  |  |   // 根据文件类型进行预览 | 
 |  |  |   const fileName = item.bucketFileName || item.name; | 
 |  |  |   const fileType = getFileType(fileName); | 
 |  |  |   const fileType = getFileType(item.url); | 
 |  |  |   const fullUrl = getFullUrl(item.url); | 
 |  |  |  | 
 |  |  |   if (fileType.startsWith("image")) { | 
 |  |  |     // 图片预览 | 
 |  |  |     uni.previewImage({ | 
 |  |  |       urls: [item.url], | 
 |  |  |       current: item.url, | 
 |  |  |       urls: [fullUrl], | 
 |  |  |       current: fullUrl, | 
 |  |  |     }); | 
 |  |  |   } else { | 
 |  |  |     // 其他文件类型,可以下载或打开 | 
 |  |  |     uni.downloadFile({ | 
 |  |  |       url: item.url, | 
 |  |  |       url: fullUrl, | 
 |  |  |       success: (res) => { | 
 |  |  |         uni.openDocument({ | 
 |  |  |           filePath: res.tempFilePath, | 
 |  |  |           success: () => { | 
 |  |  |             console.log("打开文档成功"); | 
 |  |  |             // 打开文档成功 | 
 |  |  |           }, | 
 |  |  |           fail: (error) => { | 
 |  |  |             console.error("打开文档失败:", error); | 
 |  |  | 
 |  |  | }; | 
 |  |  |  | 
 |  |  | // 获取文件类型 | 
 |  |  | const getFileType = (fileName: string) => { | 
 |  |  |   if (!fileName) return "unknown"; | 
 |  |  |   const extension = fileName.split(".").pop()?.toLowerCase(); | 
 |  |  | const getFileType = (urlOrFileName: string) => { | 
 |  |  |   if (!urlOrFileName) return "unknown"; | 
 |  |  |   const extension = getExtension(urlOrFileName); | 
 |  |  |   switch (extension) { | 
 |  |  |     case "jpg": | 
 |  |  |     case "jpeg": | 
 |  |  | 
 |  |  |     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": | 
 |  |  | 
 |  |  | } | 
 |  |  |  | 
 |  |  | .attachment-list { | 
 |  |  |   display: grid; | 
 |  |  |   grid-template-columns: repeat(3, 1fr); | 
 |  |  |   gap: 8px; | 
 |  |  |  | 
 |  |  |   .attachment-card { | 
 |  |  |     margin-bottom: 12px; | 
 |  |  |     border-radius: 4px; | 
 |  |  |     width: 100%; | 
 |  |  |     aspect-ratio: 1; | 
 |  |  |   } | 
 |  |  | } | 
 |  |  |  | 
 |  |  | .attachment-item { | 
 |  |  |   display: flex; | 
 |  |  |   align-items: center; | 
 |  |  |   padding: 12px; | 
 |  |  | .media-wrapper { | 
 |  |  |   position: relative; | 
 |  |  |   width: 100%; | 
 |  |  |   height: 100%; | 
 |  |  |   border-radius: 8px; | 
 |  |  |   overflow: hidden; | 
 |  |  |   background: #f5f5f5; | 
 |  |  |  | 
 |  |  |   .attachment-info { | 
 |  |  |     flex: 1; | 
 |  |  |   .media-preview { | 
 |  |  |     width: 100%; | 
 |  |  |     height: 100%; | 
 |  |  |     object-fit: cover; | 
 |  |  |   } | 
 |  |  |  | 
 |  |  |     .attachment-name { | 
 |  |  |       font-size: 16px; | 
 |  |  |       font-weight: 500; | 
 |  |  |       color: #333; | 
 |  |  |       margin-bottom: 4px; | 
 |  |  |       word-break: break-all; | 
 |  |  |     } | 
 |  |  |   .file-icon-wrapper { | 
 |  |  |     display: flex; | 
 |  |  |     flex-direction: column; | 
 |  |  |     align-items: center; | 
 |  |  |     justify-content: center; | 
 |  |  |     width: 100%; | 
 |  |  |     height: 100%; | 
 |  |  |     padding: 8px; | 
 |  |  |     text-align: center; | 
 |  |  |  | 
 |  |  |     .attachment-meta { | 
 |  |  |       display: flex; | 
 |  |  |       gap: 12px; | 
 |  |  |     .file-name { | 
 |  |  |       margin-top: 8px; | 
 |  |  |       font-size: 12px; | 
 |  |  |       color: #999; | 
 |  |  |       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; | 
 |  |  |       } | 
 |  |  |     } | 
 |  |  |   } | 
 |  |  |  | 
 |  |  |   .attachment-actions { | 
 |  |  |     margin-left: 12px; | 
 |  |  |  | 
 |  |  |     :deep(.wd-icon) { | 
 |  |  |       font-size: 20px; | 
 |  |  |       cursor: pointer; | 
 |  |  |     } | 
 |  |  |   .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> |