|  |  | 
 |  |  |     <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 v-for="(item, index) in attachmentList" :key="item.id || index" 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)" | 
 |  |  |               style="width: 100%; height: 100%" | 
 |  |  |               @error="onImageError(item, index)" | 
 |  |  |               @load="onImageLoad(item, index)" | 
 |  |  |               :show-menu-by-longpress="true" | 
 |  |  |             /> | 
 |  |  |             <!-- 加载中遮罩 --> | 
 |  |  |             <view v-if="item.loading" class="loading-mask"> | 
 |  |  |               <text class="loading-text">加载中...</text> | 
 |  |  |             </view> | 
 |  |  |             <!-- 图片加载失败显示默认图标 --> | 
 |  |  |             <view v-else class="file-icon-wrapper"> | 
 |  |  |             <view v-if="item.loadError" class="file-icon-wrapper error-overlay"> | 
 |  |  |               <wd-icon name="picture" size="48px" color="#ccc" /> | 
 |  |  |               <text class="file-name error-text">加载失败</text> | 
 |  |  |             </view> | 
 |  |  | 
 |  |  |           <!-- 视频预览 --> | 
 |  |  |           <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)" | 
 |  |  |               :show-center-play-btn="true" | 
 |  |  |               @error="onVideoError(item, index)" | 
 |  |  |               object-fit="cover" | 
 |  |  |             /> | 
 |  |  |             <!-- 视频加载失败显示默认图标 --> | 
 |  |  |             <view v-else class="file-icon-wrapper"> | 
 |  |  |             <view v-if="item.loadError" class="file-icon-wrapper error-overlay"> | 
 |  |  |               <wd-icon name="video" size="48px" color="#ccc" /> | 
 |  |  |               <text class="file-name error-text">加载失败</text> | 
 |  |  |             </view> | 
 |  |  | 
 |  |  | </template> | 
 |  |  |  | 
 |  |  | <script setup lang="ts"> | 
 |  |  | import { ref } from "vue"; | 
 |  |  | import { ref, computed, watch } 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; | 
 |  |  | let baseUrlValue = import.meta.env.VITE_APP_API_URL || ""; | 
 |  |  | // #ifdef H5 | 
 |  |  | baseUrl = import.meta.env.VITE_APP_BASE_API; | 
 |  |  | baseUrlValue = import.meta.env.VITE_APP_BASE_API || ""; | 
 |  |  | // #endif | 
 |  |  |  | 
 |  |  | const baseUrl = ref(baseUrlValue); // 使用ref使其在模板中可访问 | 
 |  |  |  | 
 |  |  | // 外部参数 | 
 |  |  | const props = defineProps({ | 
 |  |  | 
 |  |  | }); | 
 |  |  |  | 
 |  |  | const toast = useToast(); | 
 |  |  | const attachmentList = ref<any[]>(props.detailData.files || []); | 
 |  |  | const attachmentIds = ref<string[]>(props.detailData.attachmentId || []); | 
 |  |  |  | 
 |  |  | // 获取初始数据 | 
 |  |  | const getInitialData = () => { | 
 |  |  |   // 处理不同的数据结构 | 
 |  |  |   let data = props.detailData; | 
 |  |  |  | 
 |  |  |   // 如果是 ref 对象,获取其 value | 
 |  |  |   if (data && typeof data === "object" && "value" in data) { | 
 |  |  |     data = data.value; | 
 |  |  |   } | 
 |  |  |  | 
 |  |  |   // 如果是数组,直接返回 | 
 |  |  |   if (Array.isArray(data)) { | 
 |  |  |     return data.map((item) => ({ | 
 |  |  |       ...item, | 
 |  |  |       loading: false, | 
 |  |  |       loadError: false, | 
 |  |  |     })); | 
 |  |  |   } | 
 |  |  |  | 
 |  |  |   // 如果有 files 属性 | 
 |  |  |   if (data && data.files) { | 
 |  |  |     const files = Array.isArray(data.files) ? data.files : []; | 
 |  |  |     return files.map((item) => ({ | 
 |  |  |       ...item, | 
 |  |  |       loading: false, | 
 |  |  |       loadError: false, | 
 |  |  |     })); | 
 |  |  |   } | 
 |  |  |  | 
 |  |  |   return []; | 
 |  |  | }; | 
 |  |  |  | 
 |  |  | const attachmentList = ref<any[]>(getInitialData()); | 
 |  |  | const attachmentIds = ref<string[]>(attachmentList.value.map((item: any) => item.id) || []); | 
 |  |  |  | 
 |  |  | // 监听 props.detailData 变化 | 
 |  |  | watch( | 
 |  |  |   () => props.detailData, | 
 |  |  |   (newVal) => { | 
 |  |  |     const newData = getInitialData(); | 
 |  |  |     if (newData.length > 0) { | 
 |  |  |       attachmentList.value = newData.map((item) => ({ | 
 |  |  |         ...item, | 
 |  |  |         loading: false, | 
 |  |  |         loadError: false, | 
 |  |  |       })); | 
 |  |  |       attachmentIds.value = newData.map((item: any) => item.id); | 
 |  |  |     } | 
 |  |  |   }, | 
 |  |  |   { deep: true, immediate: false } | 
 |  |  | ); | 
 |  |  |  | 
 |  |  | // 获取完整的图片/视频 URL | 
 |  |  | const getFullUrl = (url: string) => { | 
 |  |  |   if (!url) return ""; | 
 |  |  |  | 
 |  |  |   // 如果已经是完整的 URL(http 或 https 开头),直接返回 | 
 |  |  |   if (url.startsWith("http://") || url.startsWith("https://")) { | 
 |  |  |     return url; | 
 |  |  |   } | 
 |  |  |  | 
 |  |  |   // 检查 baseUrl 是否有效 | 
 |  |  |   if (!baseUrl.value) { | 
 |  |  |     console.error("❌ baseUrl未配置,url:", url); | 
 |  |  |     return url; | 
 |  |  |   } | 
 |  |  |  | 
 |  |  |   // 如果是相对路径,拼接基础 URL | 
 |  |  |   return `${baseUrl}${url.startsWith("/") ? "" : "/"}${url}`; | 
 |  |  |   const separator = url.startsWith("/") || baseUrl.value.endsWith("/") ? "" : "/"; | 
 |  |  |   return `${baseUrl.value}${separator}${url}`; | 
 |  |  | }; | 
 |  |  |  | 
 |  |  | // 图片加载成功 | 
 |  |  | const onImageLoad = (item: any) => { | 
 |  |  | const onImageLoad = (item: any, index: number) => { | 
 |  |  |   item.loading = false; | 
 |  |  |   item.loadError = false; | 
 |  |  |   attachmentList.value = [...attachmentList.value]; | 
 |  |  | }; | 
 |  |  |  | 
 |  |  | // 图片加载失败 | 
 |  |  | const onImageError = (item: any) => { | 
 |  |  |   console.error("图片加载失败:", item.url); | 
 |  |  | const onImageError = (item: any, index: number) => { | 
 |  |  |   console.error(`图片加载失败 [${index}]:`, item.url); | 
 |  |  |   item.loading = false; | 
 |  |  |   item.loadError = true; | 
 |  |  |   attachmentList.value = [...attachmentList.value]; | 
 |  |  | }; | 
 |  |  |  | 
 |  |  | // 视频加载失败 | 
 |  |  | const onVideoError = (item: any) => { | 
 |  |  |   console.error("视频加载失败:", item.url); | 
 |  |  | const onVideoError = (item: any, index: number) => { | 
 |  |  |   console.error(`视频加载失败 [${index}]:`, item.url); | 
 |  |  |   item.loading = false; | 
 |  |  |   item.loadError = true; | 
 |  |  |   attachmentList.value = [...attachmentList.value]; | 
 |  |  | }; | 
 |  |  |  | 
 |  |  | // 新增附件 | 
 |  |  | 
 |  |  |  | 
 |  |  |   .attachment-card { | 
 |  |  |     width: 100%; | 
 |  |  |     aspect-ratio: 1; | 
 |  |  |     position: relative; | 
 |  |  |  | 
 |  |  |     // 使用 padding-top 实现正方形(兼容性更好) | 
 |  |  |     &::before { | 
 |  |  |       content: ""; | 
 |  |  |       display: block; | 
 |  |  |       padding-top: 100%; // 高度等于宽度 | 
 |  |  |     } | 
 |  |  |   } | 
 |  |  | } | 
 |  |  |  | 
 |  |  | .media-wrapper { | 
 |  |  |   position: relative; | 
 |  |  |   width: 100%; | 
 |  |  |   height: 100%; | 
 |  |  |   position: absolute; | 
 |  |  |   top: 0; | 
 |  |  |   left: 0; | 
 |  |  |   right: 0; | 
 |  |  |   bottom: 0; | 
 |  |  |   border-radius: 8px; | 
 |  |  |   overflow: hidden; | 
 |  |  |   background: #f5f5f5; | 
 |  |  | 
 |  |  |     width: 100%; | 
 |  |  |     height: 100%; | 
 |  |  |     object-fit: cover; | 
 |  |  |     display: block; | 
 |  |  |   } | 
 |  |  |  | 
 |  |  |   .loading-mask { | 
 |  |  |     position: absolute; | 
 |  |  |     top: 0; | 
 |  |  |     left: 0; | 
 |  |  |     right: 0; | 
 |  |  |     bottom: 0; | 
 |  |  |     display: flex; | 
 |  |  |     align-items: center; | 
 |  |  |     justify-content: center; | 
 |  |  |     background: rgba(0, 0, 0, 0.3); | 
 |  |  |     z-index: 5; | 
 |  |  |  | 
 |  |  |     .loading-text { | 
 |  |  |       font-size: 12px; | 
 |  |  |       color: #fff; | 
 |  |  |     } | 
 |  |  |   } | 
 |  |  |  | 
 |  |  |   .file-icon-wrapper { | 
 |  |  | 
 |  |  |         color: #ff4757; | 
 |  |  |       } | 
 |  |  |     } | 
 |  |  |  | 
 |  |  |     &.error-overlay { | 
 |  |  |       position: absolute; | 
 |  |  |       top: 0; | 
 |  |  |       left: 0; | 
 |  |  |       right: 0; | 
 |  |  |       bottom: 0; | 
 |  |  |       background: rgba(255, 255, 255, 0.9); | 
 |  |  |       z-index: 5; | 
 |  |  |     } | 
 |  |  |   } | 
 |  |  |  | 
 |  |  |   .delete-btn { |