index.vue
12.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
<!--
* @Date: 2026-01-30
* @Description: 产品详情页
-->
<template>
<div class="min-h-screen bg-[#F9FAFB] pb-[calc(160rpx+env(safe-area-inset-bottom))]">
<NavHeader title="产品详情" />
<!-- Banner Image -->
<div class="w-full h-[420rpx] relative">
<img
class="w-full h-full object-cover"
:src="bannerImage"
mode="aspectFill"
/>
<div class="absolute top-[32rpx] right-[32rpx] flex gap-[16rpx]">
<!-- Hot Tag -->
<div class="bg-red-500 text-white text-[24rpx] px-[16rpx] py-[8rpx] rounded-full shadow-sm">
热卖
</div>
<!-- Favorite Button -->
<div
class="w-[64rpx] h-[48rpx] rounded-full shadow-md flex items-center justify-center"
:class="isCollected ? 'bg-[#F59E0B]' : 'bg-white'"
@tap="toggleCollect"
>
<IconFont
:name="isCollected ? 'StarFill' : 'Star'"
:size="20"
:color="isCollected ? 'white' : '#9CA3AF'"
/>
</div>
</div>
</div>
<!-- Product Header -->
<div class="relative mt-[-40rpx] bg-white rounded-t-[40rpx] px-[40rpx] pt-[48rpx] pb-[40rpx] z-10">
<h1 class="text-[#1F2937] text-[44rpx] font-bold mb-[24rpx]">终身寿险尊享版</h1>
<div class="flex flex-wrap gap-[16rpx]">
<div class="px-[16rpx] py-[6rpx] bg-red-50 rounded-[8rpx]">
<span class="text-red-600 text-[24rpx]">收益率3.5%</span>
</div>
<div class="px-[16rpx] py-[6rpx] bg-orange-50 rounded-[8rpx]">
<span class="text-orange-600 text-[24rpx]">5年超值</span>
</div>
<div class="px-[16rpx] py-[6rpx] bg-green-50 rounded-[8rpx]">
<span class="text-green-600 text-[24rpx]">保证收益万能</span>
</div>
</div>
</div>
<!-- Stats Grid -->
<div class="px-[32rpx] mt-[24rpx]">
<div class="grid grid-cols-2 gap-[24rpx]">
<div
v-for="(item, index) in stats"
:key="index"
class="bg-white rounded-[24rpx] p-[32rpx] border border-gray-100"
>
<div class="text-[#6B7280] text-[24rpx] mb-[12rpx]">{{ item.label }}</div>
<div class="text-[#1F2937] text-[30rpx] font-medium">{{ item.value }}</div>
</div>
</div>
</div>
<!-- Product Features -->
<div class="px-[32rpx] mt-[32rpx]">
<div class="bg-white rounded-[32rpx] p-[40rpx]">
<h2 class="text-[#1F2937] text-[32rpx] font-bold mb-[32rpx]">产品特色</h2>
<div class="flex flex-col gap-[32rpx]">
<div v-for="(feature, index) in features" :key="index" class="flex items-start">
<div class="w-[48rpx] h-[48rpx] rounded-full bg-blue-50 flex items-center justify-center mr-[24rpx] flex-shrink-0">
<IconFont name="Check" size="14" color="#2563EB" />
</div>
<div class="flex-1">
<div class="text-[#1F2937] text-[28rpx] font-medium leading-[1.4]">{{ feature.title }}</div>
<div class="text-[#6B7280] text-[24rpx] mt-[8rpx] leading-[1.4]">{{ feature.desc }}</div>
</div>
</div>
</div>
</div>
</div>
<!-- Attachments -->
<div class="px-[32rpx] mt-[32rpx]">
<div class="bg-white rounded-[32rpx] p-[40rpx]">
<h2 class="text-[#1F2937] text-[32rpx] font-bold mb-[32rpx]">相关附件</h2>
<div class="flex flex-col gap-[24rpx]">
<div
v-for="(file, index) in files"
:key="index"
class="flex flex-col p-[24rpx] bg-gray-50 rounded-[16rpx]"
>
<div class="flex items-center justify-between mb-[8rpx]">
<div class="flex items-center flex-1 mr-[24rpx]">
<IconFont :name="file.iconName" size="24" :color="file.iconColor" customClass="mr-[24rpx]" />
<div class="flex flex-col">
<span class="text-[#1F2937] text-[28rpx] font-medium mb-[4rpx] line-clamp-1">{{ file.name }}</span>
<span class="text-[#9CA3AF] text-[24rpx]">{{ file.size }}</span>
</div>
</div>
<IconFont name="Download" size="20" color="#2563EB" @tap="onDownload(file)" />
</div>
</div>
</div>
</div>
</div>
<!-- TabBar -->
<TabBar />
</div>
</template>
<script setup>
import { ref } from 'vue'
import NavHeader from '@/components/NavHeader.vue'
import TabBar from '@/components/TabBar.vue'
import IconFont from '@/components/IconFont.vue'
import Taro, { useLoad } from '@tarojs/taro'
// 接收页面参数
const productId = ref(null)
useLoad((options) => {
console.log('产品详情页参数:', options)
if (options.id) {
productId.value = options.id
console.log('产品ID:', productId.value)
// TODO: 根据 productId 获取产品详情数据
// 这里可以调用 API 获取对应产品的数据
fetchProductDetail(options.id)
} else {
console.warn('未接收到产品ID')
Taro.showToast({
title: '产品ID不存在',
icon: 'none',
duration: 2000
})
}
})
// 根据 ID 获取产品详情(模拟)
const fetchProductDetail = async (id) => {
console.log('正在获取产品ID', id, '的详情...')
// TODO: 实际调用 API
// const res = await getProductDetailAPI({ i: id })
// if (res.code === 1) {
// // 更新产品数据
// }
// 模拟根据不同ID显示不同产品
const productNames = {
'1': '家庭财富传承保障计划(分红)',
'2': '儿童教育金储备方案(分红)'
}
if (productNames[id]) {
console.log('产品名称:', productNames[id])
}
}
// Random banner image
const bannerImage = `https://picsum.photos/seed/${Math.floor(Math.random() * 1000)}/750/420`
const stats = ref([
{ label: '投保年龄', value: '30天-70周岁' },
{ label: '保障期限', value: '终身' },
{ label: '缴费方式', value: '3/5/10年交' },
{ label: '起投金额', value: '10000元起' }
])
const features = ref([
{ title: '身故保险金', desc: '赔付100%基本保额,给家人留爱不留债' },
{ title: '全残保险金', desc: '赔付100%基本保额,生活有保障' },
{ title: '保费豁免', desc: '确诊重疾后免交剩余保费,保障继续有效' },
{ title: '保单贷款', desc: '最高可贷现金价值80%,资金周转灵活' }
])
const files = ref([
{
name: '产品条款.pdf',
size: '2.3MB',
fileName: '产品条款.pdf',
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',
iconName: 'Order',
iconColor: '#EF4444',
fileType: 'pdf',
showTip: false,
tipText: ''
},
{
name: '投保须知.docx',
size: '1.8MB',
fileName: '投保须知.docx',
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',
iconName: 'Order',
iconColor: '#2563EB',
fileType: 'docx',
showTip: true,
tipText: 'Word 文档可能无法正常显示,建议打开后点击右上角"..."菜单选择"发送给朋友"保存到电脑查看'
},
{
name: '健康告知.pptx',
size: '3.2MB',
fileName: '健康告知.pptx',
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',
iconName: 'Order',
iconColor: '#F59E0B',
fileType: 'pptx',
showTip: true,
tipText: 'PPT 文档可能无法正常显示,建议打开后点击右上角"..."菜单选择"发送给朋友"保存到电脑查看'
},
{
name: '保险责任说明.xlsx',
size: '1.5MB',
fileName: '保险责任说明.xlsx',
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',
iconName: 'Order',
iconColor: '#10B981',
fileType: 'xlsx',
showTip: true,
tipText: 'Excel 文档可能无法正常显示,建议打开后点击右上角"..."菜单选择"发送给朋友"保存到电脑查看'
}
])
// 收藏状态
const isCollected = ref(false)
// 记录是否已经提示过 Office 文档预览限制
const hasShownOfficeTip = ref(false)
// 切换收藏状态
const toggleCollect = () => {
isCollected.value = !isCollected.value
if (isCollected.value) {
Taro.showToast({
title: '已收藏',
icon: 'success'
})
} else {
Taro.showToast({
title: '已取消收藏',
icon: 'none'
})
}
}
// 打开文件的通用函数
const openFile = async (filePath, file) => {
try {
await Taro.openDocument({
filePath: filePath,
showMenu: true,
success: () => {
console.log('文件打开成功')
},
fail: (err) => {
console.error('打开文件失败:', err)
const fileExt = file.fileName.split('.').pop()?.toLowerCase() || ''
let message = '文件打开失败'
let suggestion = ''
if (['docx', 'doc', 'pptx', 'ppt', 'xlsx', 'xls'].includes(fileExt)) {
message = '暂不支持预览 Office 文档'
suggestion = '\n\n建议:\n1. 点击右上角"..."菜单\n2. 选择"发送给朋友"\n3. 在电脑或支持的应用中打开'
} else if (['pdf'].includes(fileExt)) {
message = 'PDF 文件打开失败'
suggestion = '\n\n文件可能已损坏,请联系管理员'
} else {
message = `暂不支持预览 ${fileExt.toUpperCase()} 格式文件`
suggestion = '\n\n请在电脑或其他应用中打开'
}
Taro.showModal({
title: '提示',
content: message + suggestion,
showCancel: false,
confirmText: '我知道了'
})
}
})
} catch (error) {
console.error('打开文件异常:', error)
Taro.showToast({
title: '打开文件失败',
icon: 'none',
duration: 2000
})
}
}
// 下载并打开文件的函数
const downloadAndOpenFile = async (file) => {
try {
const downloadResult = await Taro.downloadFile({
url: file.downloadUrl
})
if (downloadResult.statusCode !== 200) {
throw new Error(`打开失败: HTTP ${downloadResult.statusCode}`)
}
if (!downloadResult.tempFilePath) {
throw new Error('打开失败: 未获取到文件')
}
Taro.hideLoading()
const fileExt = file.fileName.split('.').pop()?.toLowerCase() || ''
const unsupportedFormats = ['docx', 'doc', 'pptx', 'ppt', 'xlsx', 'xls']
if (unsupportedFormats.includes(fileExt)) {
// 对于 Office 文档,如果还没提示过,就提示一次
if (!hasShownOfficeTip.value) {
Taro.showModal({
title: '预览提示',
content: `小程序对 ${fileExt.toUpperCase()} 文档的预览支持有限,如果显示为空白,请点击右上角"..."菜单,选择"发送给朋友"后在电脑或其他应用中打开。\n\n是否继续尝试预览?`,
confirmText: '继续',
cancelText: '取消',
success: (modalRes) => {
if (modalRes.confirm) {
hasShownOfficeTip.value = true // 标记已提示过
openFile(downloadResult.tempFilePath, file)
}
}
})
} else {
// 已经提示过,直接打开
await openFile(downloadResult.tempFilePath, file)
}
} else {
// PDF 等其他格式直接打开
await openFile(downloadResult.tempFilePath, file)
}
} catch (error) {
Taro.hideLoading()
console.error('打开文件出错:', error)
let errorMessage = '打开失败,请重试'
if (error.errMsg && error.errMsg.includes('network')) {
errorMessage = '网络连接失败,请检查网络'
} else if (error.errMsg && error.errMsg.includes('TLS')) {
errorMessage = '安全连接失败,请检查网络'
}
Taro.showToast({
title: errorMessage,
icon: 'none',
duration: 2000
})
}
}
// 下载按钮点击处理
const onDownload = (file) => {
if (!file.downloadUrl) {
Taro.showToast({
title: '该文件暂无下载地址',
icon: 'none',
duration: 2000
})
return
}
Taro.showLoading({
title: '打开中...',
mask: true
})
downloadAndOpenFile(file)
}
</script>