hookehuyr

feat(product-detail): 添加收藏功能和附件下载功能

- 头部热卖标签旁添加收藏按钮,支持切换收藏状态
- 相关附件列表添加下载功能,点击下载图标打开文件
- Mock 不同文档类型展示(PDF/Word/PPT/Excel),便于测试
- 实现智能提示系统:Office 文档同页面仅首次打开时提示
- 附件下方显示灰色提示文字,引导用户手动保存
- 完整的错误处理和网络异常提示
- 统一使用"打开中..."作为加载提示

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
...@@ -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>
......