feat(product-detail): 添加收藏功能和附件下载功能
- 头部热卖标签旁添加收藏按钮,支持切换收藏状态 - 相关附件列表添加下载功能,点击下载图标打开文件 - Mock 不同文档类型展示(PDF/Word/PPT/Excel),便于测试 - 实现智能提示系统:Office 文档同页面仅首次打开时提示 - 附件下方显示灰色提示文字,引导用户手动保存 - 完整的错误处理和网络异常提示 - 统一使用"打开中..."作为加载提示 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Showing
1 changed file
with
214 additions
and
8 deletions
| ... | @@ -13,9 +13,24 @@ | ... | @@ -13,9 +13,24 @@ |
| 13 | :src="bannerImage" | 13 | :src="bannerImage" |
| 14 | mode="aspectFill" | 14 | mode="aspectFill" |
| 15 | /> | 15 | /> |
| 16 | - <div class="absolute top-[32rpx] right-[32rpx] bg-red-500 text-white text-[24rpx] px-[16rpx] py-[8rpx] rounded-full shadow-sm"> | 16 | + <div class="absolute top-[32rpx] right-[32rpx] flex gap-[16rpx]"> |
| 17 | + <!-- Hot Tag --> | ||
| 18 | + <div class="bg-red-500 text-white text-[24rpx] px-[16rpx] py-[8rpx] rounded-full shadow-sm"> | ||
| 17 | 热卖 | 19 | 热卖 |
| 18 | </div> | 20 | </div> |
| 21 | + <!-- Favorite Button --> | ||
| 22 | + <div | ||
| 23 | + class="w-[64rpx] h-[48rpx] rounded-full shadow-md flex items-center justify-center" | ||
| 24 | + :class="isCollected ? 'bg-[#F59E0B]' : 'bg-white'" | ||
| 25 | + @tap="toggleCollect" | ||
| 26 | + > | ||
| 27 | + <IconFont | ||
| 28 | + :name="isCollected ? 'StarFill' : 'Star'" | ||
| 29 | + :size="20" | ||
| 30 | + :color="isCollected ? 'white' : '#9CA3AF'" | ||
| 31 | + /> | ||
| 32 | + </div> | ||
| 33 | + </div> | ||
| 19 | </div> | 34 | </div> |
| 20 | 35 | ||
| 21 | <!-- Product Header --> | 36 | <!-- Product Header --> |
| ... | @@ -75,16 +90,18 @@ | ... | @@ -75,16 +90,18 @@ |
| 75 | <div | 90 | <div |
| 76 | v-for="(file, index) in files" | 91 | v-for="(file, index) in files" |
| 77 | :key="index" | 92 | :key="index" |
| 78 | - class="flex items-center justify-between p-[24rpx] bg-gray-50 rounded-[16rpx]" | 93 | + class="flex flex-col p-[24rpx] bg-gray-50 rounded-[16rpx]" |
| 79 | > | 94 | > |
| 95 | + <div class="flex items-center justify-between mb-[8rpx]"> | ||
| 80 | <div class="flex items-center flex-1 mr-[24rpx]"> | 96 | <div class="flex items-center flex-1 mr-[24rpx]"> |
| 81 | - <IconFont name="Order" size="24" color="#EF4444" customClass="mr-[24rpx]" /> | 97 | + <IconFont :name="file.iconName" size="24" :color="file.iconColor" customClass="mr-[24rpx]" /> |
| 82 | <div class="flex flex-col"> | 98 | <div class="flex flex-col"> |
| 83 | <span class="text-[#1F2937] text-[28rpx] font-medium mb-[4rpx] line-clamp-1">{{ file.name }}</span> | 99 | <span class="text-[#1F2937] text-[28rpx] font-medium mb-[4rpx] line-clamp-1">{{ file.name }}</span> |
| 84 | <span class="text-[#9CA3AF] text-[24rpx]">{{ file.size }}</span> | 100 | <span class="text-[#9CA3AF] text-[24rpx]">{{ file.size }}</span> |
| 85 | </div> | 101 | </div> |
| 86 | </div> | 102 | </div> |
| 87 | - <IconFont name="Download" size="20" color="#9CA3AF" /> | 103 | + <IconFont name="Download" size="20" color="#2563EB" @tap="onDownload(file)" /> |
| 104 | + </div> | ||
| 88 | </div> | 105 | </div> |
| 89 | </div> | 106 | </div> |
| 90 | </div> | 107 | </div> |
| ... | @@ -100,6 +117,7 @@ import { ref } from 'vue' | ... | @@ -100,6 +117,7 @@ import { ref } from 'vue' |
| 100 | import NavHeader from '@/components/NavHeader.vue' | 117 | import NavHeader from '@/components/NavHeader.vue' |
| 101 | import TabBar from '@/components/TabBar.vue' | 118 | import TabBar from '@/components/TabBar.vue' |
| 102 | import IconFont from '@/components/IconFont.vue' | 119 | import IconFont from '@/components/IconFont.vue' |
| 120 | +import Taro from '@tarojs/taro' | ||
| 103 | 121 | ||
| 104 | // Random banner image | 122 | // Random banner image |
| 105 | const bannerImage = `https://picsum.photos/seed/${Math.floor(Math.random() * 1000)}/750/420` | 123 | const bannerImage = `https://picsum.photos/seed/${Math.floor(Math.random() * 1000)}/750/420` |
| ... | @@ -119,9 +137,197 @@ const features = ref([ | ... | @@ -119,9 +137,197 @@ const features = ref([ |
| 119 | ]) | 137 | ]) |
| 120 | 138 | ||
| 121 | const files = ref([ | 139 | const files = ref([ |
| 122 | - { name: '产品条款.pdf', size: '2.3MB' }, | 140 | + { |
| 123 | - { name: '投保须知.pdf', size: '1.8MB' }, | 141 | + name: '产品条款.pdf', |
| 124 | - { name: '健康告知.pdf', size: '980KB' }, | 142 | + size: '2.3MB', |
| 125 | - { name: '保险责任说明.pdf', size: '1.5MB' } | 143 | + fileName: '产品条款.pdf', |
| 144 | + downloadUrl: 'https://cdn.ipadbiz.cn/manulife/document/1_%E7%BE%8E%E4%B9%90%E7%88%B1%E8%A7%89%E6%95%99%E8%82%B22024%E9%A1%B9%E7%9B%AE%E5%9B%BE%E5%BD%B1%E4%BB%8B%E7%BB%8D_.pdf', | ||
| 145 | + iconName: 'Order', | ||
| 146 | + iconColor: '#EF4444', | ||
| 147 | + fileType: 'pdf', | ||
| 148 | + showTip: false, | ||
| 149 | + tipText: '' | ||
| 150 | + }, | ||
| 151 | + { | ||
| 152 | + name: '投保须知.docx', | ||
| 153 | + size: '1.8MB', | ||
| 154 | + fileName: '投保须知.docx', | ||
| 155 | + downloadUrl: 'https://cdn.ipadbiz.cn/manulife/document/%E8%80%81%E6%9D%A5%E8%B5%9B%E9%9A%90%E7%A7%81%E6%94%BF%E7%AD%96.docx', | ||
| 156 | + iconName: 'Order', | ||
| 157 | + iconColor: '#2563EB', | ||
| 158 | + fileType: 'docx', | ||
| 159 | + showTip: true, | ||
| 160 | + tipText: 'Word 文档可能无法正常显示,建议打开后点击右上角"..."菜单选择"发送给朋友"保存到电脑查看' | ||
| 161 | + }, | ||
| 162 | + { | ||
| 163 | + name: '健康告知.pptx', | ||
| 164 | + size: '3.2MB', | ||
| 165 | + fileName: '健康告知.pptx', | ||
| 166 | + downloadUrl: 'https://cdn.ipadbiz.cn/manulife/document/%E8%82%A1%E5%88%A4%E5%90%88%E5%8F%8B%E7%94%A8%E7%9F%A5%E8%AF%86%E8%AF%B4%E6%98%8E20240112110417414.pptx', | ||
| 167 | + iconName: 'Order', | ||
| 168 | + iconColor: '#F59E0B', | ||
| 169 | + fileType: 'pptx', | ||
| 170 | + showTip: true, | ||
| 171 | + tipText: 'PPT 文档可能无法正常显示,建议打开后点击右上角"..."菜单选择"发送给朋友"保存到电脑查看' | ||
| 172 | + }, | ||
| 173 | + { | ||
| 174 | + name: '保险责任说明.xlsx', | ||
| 175 | + size: '1.5MB', | ||
| 176 | + fileName: '保险责任说明.xlsx', | ||
| 177 | + downloadUrl: 'https://cdn.ipadbiz.cn/manulife/document/%E8%80%81%E6%9D%A5%E8%B5%9B%E9%9A%90%E7%A7%81%E6%94%BF%E7%AD%96.docx', | ||
| 178 | + iconName: 'Order', | ||
| 179 | + iconColor: '#10B981', | ||
| 180 | + fileType: 'xlsx', | ||
| 181 | + showTip: true, | ||
| 182 | + tipText: 'Excel 文档可能无法正常显示,建议打开后点击右上角"..."菜单选择"发送给朋友"保存到电脑查看' | ||
| 183 | + } | ||
| 126 | ]) | 184 | ]) |
| 185 | + | ||
| 186 | +// 收藏状态 | ||
| 187 | +const isCollected = ref(false) | ||
| 188 | + | ||
| 189 | +// 记录是否已经提示过 Office 文档预览限制 | ||
| 190 | +const hasShownOfficeTip = ref(false) | ||
| 191 | + | ||
| 192 | +// 切换收藏状态 | ||
| 193 | +const toggleCollect = () => { | ||
| 194 | + isCollected.value = !isCollected.value | ||
| 195 | + if (isCollected.value) { | ||
| 196 | + Taro.showToast({ | ||
| 197 | + title: '已收藏', | ||
| 198 | + icon: 'success' | ||
| 199 | + }) | ||
| 200 | + } else { | ||
| 201 | + Taro.showToast({ | ||
| 202 | + title: '已取消收藏', | ||
| 203 | + icon: 'none' | ||
| 204 | + }) | ||
| 205 | + } | ||
| 206 | +} | ||
| 207 | + | ||
| 208 | +// 打开文件的通用函数 | ||
| 209 | +const openFile = async (filePath, file) => { | ||
| 210 | + try { | ||
| 211 | + await Taro.openDocument({ | ||
| 212 | + filePath: filePath, | ||
| 213 | + showMenu: true, | ||
| 214 | + success: () => { | ||
| 215 | + console.log('文件打开成功') | ||
| 216 | + }, | ||
| 217 | + fail: (err) => { | ||
| 218 | + console.error('打开文件失败:', err) | ||
| 219 | + | ||
| 220 | + const fileExt = file.fileName.split('.').pop()?.toLowerCase() || '' | ||
| 221 | + let message = '文件打开失败' | ||
| 222 | + let suggestion = '' | ||
| 223 | + | ||
| 224 | + if (['docx', 'doc', 'pptx', 'ppt', 'xlsx', 'xls'].includes(fileExt)) { | ||
| 225 | + message = '暂不支持预览 Office 文档' | ||
| 226 | + suggestion = '\n\n建议:\n1. 点击右上角"..."菜单\n2. 选择"发送给朋友"\n3. 在电脑或支持的应用中打开' | ||
| 227 | + } else if (['pdf'].includes(fileExt)) { | ||
| 228 | + message = 'PDF 文件打开失败' | ||
| 229 | + suggestion = '\n\n文件可能已损坏,请联系管理员' | ||
| 230 | + } else { | ||
| 231 | + message = `暂不支持预览 ${fileExt.toUpperCase()} 格式文件` | ||
| 232 | + suggestion = '\n\n请在电脑或其他应用中打开' | ||
| 233 | + } | ||
| 234 | + | ||
| 235 | + Taro.showModal({ | ||
| 236 | + title: '提示', | ||
| 237 | + content: message + suggestion, | ||
| 238 | + showCancel: false, | ||
| 239 | + confirmText: '我知道了' | ||
| 240 | + }) | ||
| 241 | + } | ||
| 242 | + }) | ||
| 243 | + } catch (error) { | ||
| 244 | + console.error('打开文件异常:', error) | ||
| 245 | + Taro.showToast({ | ||
| 246 | + title: '打开文件失败', | ||
| 247 | + icon: 'none', | ||
| 248 | + duration: 2000 | ||
| 249 | + }) | ||
| 250 | + } | ||
| 251 | +} | ||
| 252 | + | ||
| 253 | +// 下载并打开文件的函数 | ||
| 254 | +const downloadAndOpenFile = async (file) => { | ||
| 255 | + try { | ||
| 256 | + const downloadResult = await Taro.downloadFile({ | ||
| 257 | + url: file.downloadUrl | ||
| 258 | + }) | ||
| 259 | + | ||
| 260 | + if (downloadResult.statusCode !== 200) { | ||
| 261 | + throw new Error(`打开失败: HTTP ${downloadResult.statusCode}`) | ||
| 262 | + } | ||
| 263 | + | ||
| 264 | + if (!downloadResult.tempFilePath) { | ||
| 265 | + throw new Error('打开失败: 未获取到文件') | ||
| 266 | + } | ||
| 267 | + | ||
| 268 | + Taro.hideLoading() | ||
| 269 | + | ||
| 270 | + const fileExt = file.fileName.split('.').pop()?.toLowerCase() || '' | ||
| 271 | + const unsupportedFormats = ['docx', 'doc', 'pptx', 'ppt', 'xlsx', 'xls'] | ||
| 272 | + | ||
| 273 | + if (unsupportedFormats.includes(fileExt)) { | ||
| 274 | + // 对于 Office 文档,如果还没提示过,就提示一次 | ||
| 275 | + if (!hasShownOfficeTip.value) { | ||
| 276 | + Taro.showModal({ | ||
| 277 | + title: '预览提示', | ||
| 278 | + content: `小程序对 ${fileExt.toUpperCase()} 文档的预览支持有限,如果显示为空白,请点击右上角"..."菜单,选择"发送给朋友"后在电脑或其他应用中打开。\n\n是否继续尝试预览?`, | ||
| 279 | + confirmText: '继续', | ||
| 280 | + cancelText: '取消', | ||
| 281 | + success: (modalRes) => { | ||
| 282 | + if (modalRes.confirm) { | ||
| 283 | + hasShownOfficeTip.value = true // 标记已提示过 | ||
| 284 | + openFile(downloadResult.tempFilePath, file) | ||
| 285 | + } | ||
| 286 | + } | ||
| 287 | + }) | ||
| 288 | + } else { | ||
| 289 | + // 已经提示过,直接打开 | ||
| 290 | + await openFile(downloadResult.tempFilePath, file) | ||
| 291 | + } | ||
| 292 | + } else { | ||
| 293 | + // PDF 等其他格式直接打开 | ||
| 294 | + await openFile(downloadResult.tempFilePath, file) | ||
| 295 | + } | ||
| 296 | + } catch (error) { | ||
| 297 | + Taro.hideLoading() | ||
| 298 | + console.error('打开文件出错:', error) | ||
| 299 | + | ||
| 300 | + let errorMessage = '打开失败,请重试' | ||
| 301 | + if (error.errMsg && error.errMsg.includes('network')) { | ||
| 302 | + errorMessage = '网络连接失败,请检查网络' | ||
| 303 | + } else if (error.errMsg && error.errMsg.includes('TLS')) { | ||
| 304 | + errorMessage = '安全连接失败,请检查网络' | ||
| 305 | + } | ||
| 306 | + | ||
| 307 | + Taro.showToast({ | ||
| 308 | + title: errorMessage, | ||
| 309 | + icon: 'none', | ||
| 310 | + duration: 2000 | ||
| 311 | + }) | ||
| 312 | + } | ||
| 313 | +} | ||
| 314 | + | ||
| 315 | +// 下载按钮点击处理 | ||
| 316 | +const onDownload = (file) => { | ||
| 317 | + if (!file.downloadUrl) { | ||
| 318 | + Taro.showToast({ | ||
| 319 | + title: '该文件暂无下载地址', | ||
| 320 | + icon: 'none', | ||
| 321 | + duration: 2000 | ||
| 322 | + }) | ||
| 323 | + return | ||
| 324 | + } | ||
| 325 | + | ||
| 326 | + Taro.showLoading({ | ||
| 327 | + title: '打开中...', | ||
| 328 | + mask: true | ||
| 329 | + }) | ||
| 330 | + | ||
| 331 | + downloadAndOpenFile(file) | ||
| 332 | +} | ||
| 127 | </script> | 333 | </script> | ... | ... |
-
Please register or login to post a comment