index.vue
6.15 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
<!--
* @Date: 2026-01-30
* @Description: 产品详情页
-->
<template>
<div class="min-h-screen bg-[#F9FAFB] pb-[calc(160rpx+env(safe-area-inset-bottom))]">
<NavHeader title="产品详情" />
<!-- Loading State -->
<div v-if="loading" class="flex items-center justify-center h-screen">
<div class="flex flex-col items-center">
<div class="w-[64rpx] h-[64rpx] border-4 border-gray-200 border-t-blue-600 rounded-full animate-spin mb-[24rpx]"></div>
<text class="text-gray-600 text-[28rpx]">加载中...</text>
</div>
</div>
<!-- Content -->
<div v-else>
<!-- Banner Image -->
<div class="w-full h-[420rpx] relative">
<img
class="w-full h-full object-cover"
:src="productDetail.cover_image || bannerImage"
mode="aspectFill"
/>
<div class="absolute top-[32rpx] right-[32rpx] flex items-center gap-[16rpx]">
<!-- Hot Tag -->
<div
v-if="productDetail.recommend === 'hot'"
class="bg-red-500 text-white text-[24rpx] px-[20rpx] py-[10rpx] rounded-full shadow-sm backdrop-blur-sm bg-opacity-90"
>
热卖
</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 leading-[1.2] mb-[24rpx]">
{{ productDetail.product_name || '产品详情' }}
</h1>
<!-- 动态标签 -->
<div v-if="productDetail.tags && productDetail.tags.length" class="flex flex-wrap gap-[16rpx]">
<div
v-for="tag in productDetail.tags"
:key="tag.id"
class="rounded-[8rpx] px-[16rpx] py-[6rpx]"
:style="{
backgroundColor: tag.bg_color,
color: tag.text_color
}"
>
<span class="text-[24rpx]">{{ tag.name }}</span>
</div>
</div>
</div>
<!-- Product Description (富文本) -->
<div v-if="productDetail.product_description" class="px-[32rpx] mt-[32rpx]">
<div class="bg-white rounded-[32rpx] p-[40rpx]">
<h2 class="text-[#1F2937] text-[32rpx] font-bold mb-[32rpx]">产品描述</h2>
<!-- 使用 rich-text 渲染富文本 -->
<rich-text :nodes="productDetail.product_description" class="text-[#4B5563] text-[28rpx] leading-[1.6]"></rich-text>
</div>
</div>
<!-- Attachments -->
<div v-if="productDetail.documents && productDetail.documents.length" class="px-[32rpx] mt-[32rpx]">
<div class="bg-white rounded-[32rpx] p-[40rpx]">
<h2 class="text-[#1F2937] text-[32rpx] font-bold mb-[16rpx]">相关附件</h2>
<text class="text-[#9CA3AF] text-[24rpx] mb-[24rpx] block">如无法下载,可在预览页点击右上角「...」转发至其他设备</text>
<div class="flex flex-col gap-[24rpx]">
<div
v-for="(doc, index) in productDetail.documents"
:key="index"
class="flex flex-col p-[24rpx] bg-gray-50 rounded-[16rpx]"
>
<div class="flex items-center justify-between">
<div class="flex items-center flex-1 mr-[24rpx]">
<image
:src="getDocumentIcon(doc.file_name)"
class="w-[48rpx] h-[48rpx] mr-[24rpx]"
mode="aspectFit"
/>
<div class="flex flex-col">
<span class="text-[#1F2937] text-[28rpx] font-medium mb-[4rpx] line-clamp-1">{{ doc.file_name }}</span>
<span class="text-[#9CA3AF] text-[24rpx]">{{ doc.file_size_formatted }}</span>
</div>
</div>
<IconFont name="eye" size="20" color="#2563EB" @tap="viewDocument(doc)" />
</div>
</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 { useFileOperation } from '@/composables/useFileOperation'
import Taro, { useLoad } from '@tarojs/taro'
import { getDocumentIcon } from '@/utils/documentIcons'
import { detailAPI } from '@/api/get_product'
const { viewFile } = useFileOperation()
// 接收页面参数
const productId = ref(null)
// 加载状态
const loading = ref(true)
// 产品详情数据
const productDetail = ref({
id: null,
product_name: '',
recommend: '',
product_description: '',
cover_image: '',
tags: [],
documents: []
})
/**
* 获取产品详情
*
* @description 调用 detailAPI 获取产品详情数据
* @param {string} id - 产品ID
*/
const fetchProductDetail = async (id) => {
try {
loading.value = true
const res = await detailAPI({
i: id
})
if (res.code === 1 && res.data) {
productDetail.value = res.data
} else {
Taro.showToast({
title: res.msg || '获取产品详情失败',
icon: 'none',
duration: 2000
})
}
} catch (err) {
console.error('获取产品详情失败:', err)
Taro.showToast({
title: '网络错误,请重试',
icon: 'none',
duration: 2000
})
} finally {
loading.value = false
}
}
/**
* 查看文档
*
* @description 打开文档预览
* @param {Object} doc - 文档对象
*/
const viewDocument = (doc) => {
viewFile({
fileName: doc.file_name,
downloadUrl: doc.file_url
})
}
useLoad((options) => {
console.log('产品详情页参数:', options)
if (options.id) {
productId.value = options.id
console.log('产品ID:', productId.value)
// 获取产品详情数据
fetchProductDetail(options.id)
} else {
console.warn('未接收到产品ID')
loading.value = false
Taro.showToast({
title: '产品ID不存在',
icon: 'none',
duration: 2000
})
}
})
// Random banner image (fallback)
const bannerImage = `https://picsum.photos/seed/${Math.floor(Math.random() * 1000)}/750/420`
</script>