| | |
| | | |
| | | <!-- 附件列表 --> |
| | | <view class="attachment-list"> |
| | | <wd-status-tip v-if="attachmentList.length === 0" image="content" tip="暂无附件" /> |
| | | <wd-status-tip |
| | | v-if="attachmentList.length === 0" |
| | | image="content" |
| | | tip="暂无附件" |
| | | custom-class="status-tip-full" |
| | | /> |
| | | |
| | | <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]; |
| | | }; |
| | | |
| | | // 新增附件 |
| | | const addAttachment = () => { |
| | | // 显示选择文件类型的弹窗 |
| | | uni.showActionSheet({ |
| | | itemList: ["选择图片", "选择视频", "拍照", "录像"], |
| | | itemList: ["选择图片", /* "选择视频", */ "拍照" /* , "录像" */], |
| | | success: (res) => { |
| | | switch (res.tapIndex) { |
| | | case 0: // 选择图片 |
| | | chooseImages(); |
| | | break; |
| | | case 1: // 选择视频 |
| | | chooseVideos(); |
| | | break; |
| | | case 2: // 拍照 |
| | | // case 1: // 选择视频 |
| | | // chooseVideos(); |
| | | // break; |
| | | case 1: // 拍照 |
| | | takePhoto(); |
| | | break; |
| | | case 3: // 录像 |
| | | recordVideo(); |
| | | break; |
| | | // case 3: // 录像 |
| | | // recordVideo(); |
| | | // break; |
| | | } |
| | | }, |
| | | fail: (error) => { |
| | |
| | | grid-template-columns: repeat(3, 1fr); |
| | | gap: 8px; |
| | | |
| | | :deep(.status-tip-full) { |
| | | grid-column: 1 / -1; |
| | | width: 100%; |
| | | } |
| | | |
| | | .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 { |