hookehuyr

feat(文件下载): 添加下载失败提示弹窗和手动复制链接功能

当文件自动下载失败时,显示包含文件名和链接的弹窗,并提供复制链接功能
改进下载逻辑,先尝试直接下载,失败后使用axios下载并处理各种错误情况
...@@ -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 /**
......