| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <view class="attachment-container"> |
| | | <!-- 头鍿ä½åº --> |
| | | <view class="header-actions"> |
| | | <wd-button |
| | | icon="file-add" |
| | | :round="false" |
| | | size="small" |
| | | custom-class="add_btn" |
| | | @click="addAttachment" |
| | | v-if="isEdit" |
| | | > |
| | | æ°å¢ |
| | | </wd-button> |
| | | </view> |
| | | |
| | | <!-- éä»¶å表 --> |
| | | <view class="attachment-list"> |
| | | <wd-status-tip |
| | | v-if="attachmentList.length === 0" |
| | | image="content" |
| | | tip="ææ éä»¶" |
| | | custom-class="status-tip-full" |
| | | /> |
| | | |
| | | <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 |
| | | :src="getFullUrl(item.url)" |
| | | mode="aspectFill" |
| | | class="media-preview" |
| | | 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-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> |
| | | |
| | | <!-- è§é¢é¢è§ --> |
| | | <template v-else-if="isVideoType(item.url)"> |
| | | <video |
| | | :src="getFullUrl(item.url)" |
| | | class="media-preview" |
| | | :controls="false" |
| | | :show-center-play-btn="true" |
| | | @error="onVideoError(item, index)" |
| | | object-fit="cover" |
| | | /> |
| | | <!-- è§é¢å 载失败æ¾ç¤ºé»è®¤å¾æ --> |
| | | <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> |
| | | |
| | | <!-- å
¶ä»æä»¶ç±»åæ¾ç¤ºå¾æ --> |
| | | <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)" v-if="isEdit"> |
| | | <wd-icon name="delete" color="#fff" size="20px" /> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <wd-toast /> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup lang="ts"> |
| | | 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 baseUrlValue = import.meta.env.VITE_APP_API_URL || ""; |
| | | // #ifdef H5 |
| | | baseUrlValue = import.meta.env.VITE_APP_BASE_API || ""; |
| | | // #endif |
| | | |
| | | const baseUrl = ref(baseUrlValue); // 使ç¨ref使å
¶å¨æ¨¡æ¿ä¸å¯è®¿é® |
| | | |
| | | // å¤é¨åæ° |
| | | const props = defineProps({ |
| | | detailData: { type: Object, default: () => ({}) }, |
| | | isEdit: { type: Boolean, default: false }, |
| | | deviceType: { type: String, default: "" }, |
| | | }); |
| | | |
| | | const toast = useToast(); |
| | | |
| | | // è·ååå§æ°æ® |
| | | 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 |
| | | const separator = url.startsWith("/") || baseUrl.value.endsWith("/") ? "" : "/"; |
| | | return `${baseUrl.value}${separator}${url}`; |
| | | }; |
| | | |
| | | // å¾çå è½½æå |
| | | const onImageLoad = (item: any, index: number) => { |
| | | item.loading = false; |
| | | item.loadError = false; |
| | | attachmentList.value = [...attachmentList.value]; |
| | | }; |
| | | |
| | | // å¾çå 载失败 |
| | | 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, index: number) => { |
| | | console.error(`è§é¢å 载失败 [${index}]:`, item.url); |
| | | item.loading = false; |
| | | item.loadError = true; |
| | | attachmentList.value = [...attachmentList.value]; |
| | | }; |
| | | |
| | | // æ°å¢éä»¶ |
| | | const addAttachment = () => { |
| | | // æ¾ç¤ºéæ©æä»¶ç±»åçå¼¹çª |
| | | uni.showActionSheet({ |
| | | itemList: ["éæ©å¾ç", /* "éæ©è§é¢", */ "æç
§" /* , "å½å" */], |
| | | success: (res) => { |
| | | switch (res.tapIndex) { |
| | | case 0: // éæ©å¾ç |
| | | chooseImages(); |
| | | break; |
| | | // case 1: // éæ©è§é¢ |
| | | // chooseVideos(); |
| | | // break; |
| | | case 1: // æç
§ |
| | | 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; |
| | | }); |
| | | |
| | | // æ´æ°éä»¶å表 |
| | | const flattenedResult = result.flat(); |
| | | attachmentList.value.push(...flattenedResult); |
| | | |
| | | // æåéä»¶ID |
| | | attachmentIds.value = attachmentList.value.map((item: any) => item.id); |
| | | 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 |
| | | attachmentIds.value = attachmentList.value.map((item) => item.id); |
| | | 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("ä¸è½½æä»¶å¤±è´¥"); |
| | | }, |
| | | }); |
| | | } |
| | | }; |
| | | |
| | | // ä» 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 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(); |
| | | }; |
| | | // 坹夿´é²æ¹æ³ï¼è·åææéæäº¤çæä»¶ |
| | | const getSubmitFiles = () => ({ |
| | | newFiles: attachmentIds.value || [], |
| | | }); |
| | | defineExpose({ getSubmitFiles }); |
| | | </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; |
| | | |
| | | :deep(.status-tip-full) { |
| | | grid-column: 1 / -1; |
| | | width: 100%; |
| | | } |
| | | |
| | | .attachment-card { |
| | | width: 100%; |
| | | position: relative; |
| | | |
| | | // ä½¿ç¨ padding-top å®ç°æ£æ¹å½¢ï¼å
¼å®¹æ§æ´å¥½ï¼ |
| | | &::before { |
| | | content: ""; |
| | | display: block; |
| | | padding-top: 100%; // é«åº¦çäºå®½åº¦ |
| | | } |
| | | } |
| | | } |
| | | |
| | | .media-wrapper { |
| | | position: absolute; |
| | | top: 0; |
| | | left: 0; |
| | | right: 0; |
| | | bottom: 0; |
| | | border-radius: 8px; |
| | | overflow: hidden; |
| | | background: #f5f5f5; |
| | | |
| | | .media-preview { |
| | | 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 { |
| | | 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; |
| | | } |
| | | } |
| | | |
| | | &.error-overlay { |
| | | position: absolute; |
| | | top: 0; |
| | | left: 0; |
| | | right: 0; |
| | | bottom: 0; |
| | | background: rgba(255, 255, 255, 0.9); |
| | | z-index: 5; |
| | | } |
| | | } |
| | | |
| | | .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> |