feat(文件下载): 添加下载失败提示弹窗和手动复制链接功能
当文件自动下载失败时,显示包含文件名和链接的弹窗,并提供复制链接功能 改进下载逻辑,先尝试直接下载,失败后使用axios下载并处理各种错误情况
Showing
1 changed file
with
188 additions
and
17 deletions
| ... | @@ -305,6 +305,60 @@ | ... | @@ -305,6 +305,60 @@ |
| 305 | </template> | 305 | </template> |
| 306 | </div> | 306 | </div> |
| 307 | </van-popup> | 307 | </van-popup> |
| 308 | + | ||
| 309 | + <!-- 下载失败提示弹窗 --> | ||
| 310 | + <van-popup | ||
| 311 | + v-model:show="showDownloadFailDialog" | ||
| 312 | + position="center" | ||
| 313 | + round | ||
| 314 | + closeable | ||
| 315 | + :style="{ width: '85%', maxWidth: '400px' }" | ||
| 316 | + > | ||
| 317 | + <div class="p-6"> | ||
| 318 | + <div class="text-center mb-4"> | ||
| 319 | + <van-icon name="warning-o" size="48" color="#ff6b6b" class="mb-2" /> | ||
| 320 | + <h3 class="text-lg font-medium text-gray-800">下载失败</h3> | ||
| 321 | + </div> | ||
| 322 | + | ||
| 323 | + <div class="text-center text-gray-600 mb-4"> | ||
| 324 | + <p class="mb-2">暂时无法自动下载文件</p> | ||
| 325 | + <p class="text-sm">请复制下方链接手动下载</p> | ||
| 326 | + </div> | ||
| 327 | + | ||
| 328 | + <div class="mb-4"> | ||
| 329 | + <div class="text-sm text-gray-500 mb-2">文件名:</div> | ||
| 330 | + <div class="bg-gray-50 p-3 rounded-lg text-sm break-all"> | ||
| 331 | + {{ downloadFailInfo.fileName }} | ||
| 332 | + </div> | ||
| 333 | + </div> | ||
| 334 | + | ||
| 335 | + <div class="mb-6"> | ||
| 336 | + <div class="text-sm text-gray-500 mb-2">文件链接:</div> | ||
| 337 | + <div class="bg-gray-50 p-3 rounded-lg text-sm break-all max-h-20 overflow-y-auto"> | ||
| 338 | + {{ downloadFailInfo.fileUrl }} | ||
| 339 | + </div> | ||
| 340 | + </div> | ||
| 341 | + | ||
| 342 | + <div class="flex gap-3"> | ||
| 343 | + <van-button | ||
| 344 | + block | ||
| 345 | + type="default" | ||
| 346 | + @click="showDownloadFailDialog = false" | ||
| 347 | + class="flex-1" | ||
| 348 | + > | ||
| 349 | + 关闭 | ||
| 350 | + </van-button> | ||
| 351 | + <van-button | ||
| 352 | + block | ||
| 353 | + type="primary" | ||
| 354 | + @click="copyToClipboard(downloadFailInfo.fileUrl)" | ||
| 355 | + class="flex-1" | ||
| 356 | + > | ||
| 357 | + 复制链接 | ||
| 358 | + </van-button> | ||
| 359 | + </div> | ||
| 360 | + </div> | ||
| 361 | + </van-popup> | ||
| 308 | </div> | 362 | </div> |
| 309 | </template> | 363 | </template> |
| 310 | 364 | ||
| ... | @@ -734,6 +788,71 @@ watch(showCommentPopup, async (newVal) => { | ... | @@ -734,6 +788,71 @@ watch(showCommentPopup, async (newVal) => { |
| 734 | } | 788 | } |
| 735 | }); | 789 | }); |
| 736 | 790 | ||
| 791 | +// 下载文件失败提示弹窗状态 | ||
| 792 | +const showDownloadFailDialog = ref(false); | ||
| 793 | +const downloadFailInfo = ref({ | ||
| 794 | + fileName: '', | ||
| 795 | + fileUrl: '' | ||
| 796 | +}); | ||
| 797 | + | ||
| 798 | +/** | ||
| 799 | + * 复制文件URL到剪贴板 | ||
| 800 | + * @param {string} url - 要复制的URL | ||
| 801 | + */ | ||
| 802 | +const copyToClipboard = async (url) => { | ||
| 803 | + try { | ||
| 804 | + if (navigator.clipboard && window.isSecureContext) { | ||
| 805 | + // 现代浏览器支持的方式 | ||
| 806 | + await navigator.clipboard.writeText(url); | ||
| 807 | + showToast('文件链接已复制到剪贴板'); | ||
| 808 | + } else { | ||
| 809 | + // 兼容旧浏览器的方式 | ||
| 810 | + const textArea = document.createElement('textarea'); | ||
| 811 | + textArea.value = url; | ||
| 812 | + textArea.style.position = 'fixed'; | ||
| 813 | + textArea.style.left = '-999999px'; | ||
| 814 | + textArea.style.top = '-999999px'; | ||
| 815 | + document.body.appendChild(textArea); | ||
| 816 | + textArea.focus(); | ||
| 817 | + textArea.select(); | ||
| 818 | + | ||
| 819 | + try { | ||
| 820 | + document.execCommand('copy'); | ||
| 821 | + showToast('文件链接已复制到剪贴板'); | ||
| 822 | + } catch (err) { | ||
| 823 | + console.error('复制失败:', err); | ||
| 824 | + showToast('复制失败,请手动复制链接'); | ||
| 825 | + } finally { | ||
| 826 | + document.body.removeChild(textArea); | ||
| 827 | + } | ||
| 828 | + } | ||
| 829 | + } catch (err) { | ||
| 830 | + console.error('复制到剪贴板失败:', err); | ||
| 831 | + showToast('复制失败,请手动复制链接'); | ||
| 832 | + } | ||
| 833 | +}; | ||
| 834 | + | ||
| 835 | +/** | ||
| 836 | + * 尝试直接下载文件(适用于同源或支持CORS的文件) | ||
| 837 | + * @param {string} fileUrl - 文件URL | ||
| 838 | + * @param {string} fileName - 文件名 | ||
| 839 | + */ | ||
| 840 | +const tryDirectDownload = (fileUrl, fileName) => { | ||
| 841 | + try { | ||
| 842 | + const a = document.createElement('a'); | ||
| 843 | + a.href = fileUrl; | ||
| 844 | + a.download = fileName; | ||
| 845 | + a.style.display = 'none'; | ||
| 846 | + document.body.appendChild(a); | ||
| 847 | + a.click(); | ||
| 848 | + document.body.removeChild(a); | ||
| 849 | + return true; | ||
| 850 | + } catch (error) { | ||
| 851 | + console.error('直接下载失败:', error); | ||
| 852 | + return false; | ||
| 853 | + } | ||
| 854 | +}; | ||
| 855 | + | ||
| 737 | // 下载文件 | 856 | // 下载文件 |
| 738 | const downloadFile = ({ title, url, meta_id }) => { | 857 | const downloadFile = ({ title, url, meta_id }) => { |
| 739 | // 获取文件URL和文件名 | 858 | // 获取文件URL和文件名 |
| ... | @@ -769,33 +888,85 @@ const downloadFile = ({ title, url, meta_id }) => { | ... | @@ -769,33 +888,85 @@ const downloadFile = ({ title, url, meta_id }) => { |
| 769 | return mimeTypes[extension] || 'application/octet-stream'; | 888 | return mimeTypes[extension] || 'application/octet-stream'; |
| 770 | }; | 889 | }; |
| 771 | 890 | ||
| 891 | + // 首先尝试直接下载(适用于同源文件或支持下载的链接) | ||
| 892 | + const directDownloadSuccess = tryDirectDownload(fileUrl, fileName); | ||
| 893 | + | ||
| 894 | + // 如果直接下载可能成功,等待一段时间后检查是否真的成功 | ||
| 895 | + if (directDownloadSuccess) { | ||
| 896 | + // 记录下载行为 | ||
| 897 | + let paramsObj = { | ||
| 898 | + schedule_id: courseId.value, | ||
| 899 | + meta_id | ||
| 900 | + } | ||
| 901 | + addRecord(paramsObj); | ||
| 902 | + return; | ||
| 903 | + } | ||
| 904 | + | ||
| 905 | + // 如果直接下载失败,尝试通过axios下载 | ||
| 772 | axios({ | 906 | axios({ |
| 773 | method: 'get', | 907 | method: 'get', |
| 774 | url: fileUrl, | 908 | url: fileUrl, |
| 775 | - responseType: 'blob' // 表示返回的数据类型是Blob | 909 | + responseType: 'blob', // 表示返回的数据类型是Blob |
| 910 | + timeout: 30000 // 设置30秒超时 | ||
| 776 | }).then((response) => { | 911 | }).then((response) => { |
| 777 | - const blob = new Blob([response.data], { type: getMimeType(fileUrl) }); | 912 | + try { |
| 778 | - const url = window.URL.createObjectURL(blob); | 913 | + const blob = new Blob([response.data], { type: getMimeType(fileUrl) }); |
| 914 | + const blobUrl = window.URL.createObjectURL(blob); | ||
| 915 | + | ||
| 916 | + const a = document.createElement('a'); | ||
| 917 | + a.href = blobUrl; | ||
| 918 | + a.download = fileName; | ||
| 919 | + a.style.display = 'none'; | ||
| 920 | + document.body.appendChild(a); | ||
| 921 | + a.click(); | ||
| 922 | + document.body.removeChild(a); | ||
| 923 | + | ||
| 924 | + // 延迟释放URL,确保下载完成 | ||
| 925 | + setTimeout(() => { | ||
| 926 | + window.URL.revokeObjectURL(blobUrl); | ||
| 927 | + }, 1000); | ||
| 779 | 928 | ||
| 780 | - const a = document.createElement('a'); | 929 | + // 新增记录 |
| 781 | - a.href = url; | 930 | + let paramsObj = { |
| 782 | - a.download = fileName; | 931 | + schedule_id: courseId.value, |
| 783 | - a.style.display = 'none'; | 932 | + meta_id |
| 784 | - document.body.appendChild(a); | 933 | + } |
| 785 | - a.click(); | 934 | + addRecord(paramsObj); |
| 786 | - document.body.removeChild(a); | ||
| 787 | 935 | ||
| 788 | - window.URL.revokeObjectURL(url); | 936 | + showToast('文件下载已开始'); |
| 789 | - // 新增记录 | 937 | + } catch (blobError) { |
| 790 | - let paramsObj = { | 938 | + console.error('创建下载链接失败:', blobError); |
| 791 | - schedule_id: courseId.value, | 939 | + // 显示下载失败提示 |
| 792 | - meta_id | 940 | + downloadFailInfo.value = { |
| 941 | + fileName: fileName, | ||
| 942 | + fileUrl: fileUrl | ||
| 943 | + }; | ||
| 944 | + showDownloadFailDialog.value = true; | ||
| 793 | } | 945 | } |
| 794 | - addRecord(paramsObj); | ||
| 795 | }).catch((error) => { | 946 | }).catch((error) => { |
| 796 | console.error('下载文件出错:', error); | 947 | console.error('下载文件出错:', error); |
| 797 | - }); | ||
| 798 | 948 | ||
| 949 | + // 根据错误类型提供不同的处理方式 | ||
| 950 | + let errorMessage = '下载失败'; | ||
| 951 | + if (error.code === 'ECONNABORTED' || error.message.includes('timeout')) { | ||
| 952 | + errorMessage = '下载超时'; | ||
| 953 | + } else if (error.response && error.response.status === 404) { | ||
| 954 | + errorMessage = '文件不存在'; | ||
| 955 | + } else if (error.response && error.response.status === 403) { | ||
| 956 | + errorMessage = '无权限访问文件'; | ||
| 957 | + } else if (error.message.includes('CORS') || error.message.includes('cross-origin')) { | ||
| 958 | + errorMessage = '跨域访问限制'; | ||
| 959 | + } | ||
| 960 | + | ||
| 961 | + console.log(`${errorMessage},显示手动下载提示`); | ||
| 962 | + | ||
| 963 | + // 显示下载失败提示弹窗 | ||
| 964 | + downloadFailInfo.value = { | ||
| 965 | + fileName: fileName, | ||
| 966 | + fileUrl: fileUrl | ||
| 967 | + }; | ||
| 968 | + showDownloadFailDialog.value = true; | ||
| 969 | + }); | ||
| 799 | } | 970 | } |
| 800 | 971 | ||
| 801 | /** | 972 | /** | ... | ... |
-
Please register or login to post a comment