hookehuyr

feat(推荐车辆列表): 实现推荐车辆列表接口对接及UI优化

- 对接获取推荐车辆、品牌和学校接口
- 替换静态数据为接口返回的动态数据
- 优化车辆卡片UI显示,增加原价显示和推荐标签
- 添加空状态显示和加载更多功能
- 实现筛选条件和搜索功能
...@@ -46,18 +46,19 @@ ...@@ -46,18 +46,19 @@
46 :lower-threshold="50" 46 :lower-threshold="50"
47 :enable-flex="false" 47 :enable-flex="false"
48 > 48 >
49 - <view class="space-y-4"> 49 + <!-- 车辆列表 -->
50 + <view v-if="recommendCars.length > 0" class="space-y-4">
50 <view v-for="car in recommendCars" :key="car.id" 51 <view v-for="car in recommendCars" :key="car.id"
51 class="bg-white rounded-lg shadow-sm overflow-hidden mb-3" 52 class="bg-white rounded-lg shadow-sm overflow-hidden mb-3"
52 @tap="() => onCarClick(car)" 53 @tap="() => onCarClick(car)"
53 > 54 >
54 <view class="flex"> 55 <view class="flex">
55 <view class="w-32 h-24 relative p-2"> 56 <view class="w-32 h-24 relative p-2">
56 - <image :src="car.imageUrl" :alt="car.name" mode="aspectFill" 57 + <image :src="car.front_photo" :alt="car.name" mode="aspectFill"
57 class="w-full h-full object-cover rounded-lg" /> 58 class="w-full h-full object-cover rounded-lg" />
58 - <view v-if="car.isNew" 59 + <view v-if="car.isRecommend"
59 - class="absolute bottom-3 right-3 bg-red-500 text-white text-xs px-1 rounded flex items-center"> 60 + class="absolute bottom-3 right-3 bg-green-500 text-white text-xs px-1 rounded flex items-center">
60 - <text class="text-white"></text> 61 + <text class="text-white"></text>
61 </view> 62 </view>
62 </view> 63 </view>
63 <view class="flex-1 p-3 relative"> 64 <view class="flex-1 p-3 relative">
...@@ -65,27 +66,42 @@ ...@@ -65,27 +66,42 @@
65 <Heart1 v-if="!car.is_favorite" size="22" color="#9ca3af" /> 66 <Heart1 v-if="!car.is_favorite" size="22" color="#9ca3af" />
66 <HeartFill v-else size="22" color="#ef4444" /> 67 <HeartFill v-else size="22" color="#ef4444" />
67 </view> 68 </view>
68 - <text class="font-medium text-sm block">{{ car.name }}</text> 69 + <text class="font-medium text-sm block">{{ car.brand }} {{ car.model }}</text>
69 <text class="text-xs text-gray-600 mt-1 block"> 70 <text class="text-xs text-gray-600 mt-1 block">
70 - {{ car.year }} · 71 + {{ car.manufacture_year }} ·
71 - <text v-if="car.batteryHealth">电池健康度{{ car.batteryHealth }}%</text> 72 + <text v-if="car.range_km">续航{{ car.range_km }}km</text>
72 - <text v-if="car.mileage"> 行驶{{ car.mileage }}公里</text> 73 + <text v-if="car.max_speed_kmh"> 最高时速{{ car.max_speed_kmh }}km/h</text>
73 </text> 74 </text>
74 <view class="mt-2"> 75 <view class="mt-2">
75 - <text class="text-orange-500 font-bold" style="font-size: 1.2rem;"> 76 + <!-- 原价和现价 -->
76 - ¥{{ car.price.toLocaleString() }} 77 + <view class="flex items-center">
77 - </text> 78 + <text v-if="car.market_price" class="text-xs text-gray-400 line-through mr-2">
78 - <text class="text-xs text-gray-500 mt-1 block">{{ car.school }}</text> 79 + ¥{{ car.market_price.toLocaleString() }}
80 + </text>
81 + <text class="text-orange-500 font-bold" style="font-size: 1.2rem;">
82 + ¥{{ car.price.toLocaleString() }}
83 + </text>
84 + </view>
85 + <text class="text-xs text-gray-500 mt-1 block">{{ car.school_name }}</text>
79 </view> 86 </view>
80 <!-- 推荐理由 --> 87 <!-- 推荐理由 -->
81 <view class="mt-1"> 88 <view class="mt-1">
82 - <text class="text-xs text-green-600">{{ car.recommendReason }}</text> 89 + <text class="text-xs text-green-600">精品推荐</text>
83 </view> 90 </view>
84 </view> 91 </view>
85 </view> 92 </view>
86 </view> 93 </view>
87 </view> 94 </view>
88 95
96 + <!-- 空状态显示 -->
97 + <view v-else-if="!loading" class="empty-state flex flex-col items-center justify-center py-20">
98 + <!-- <view class="empty-icon mb-4">
99 + <Search2 size="40" color="#9ca3af" />
100 + </view> -->
101 + <text class="text-gray-400 text-lg mb-2">暂无精品推荐车辆</text>
102 + <text class="text-gray-300 text-sm">换个筛选条件试试吧</text>
103 + </view>
104 +
89 <!-- Loading indicator --> 105 <!-- Loading indicator -->
90 <view v-if="loading" class="loading-container py-4 text-center"> 106 <view v-if="loading" class="loading-container py-4 text-center">
91 <text class="loading-text text-gray-500">加载中...</text> 107 <text class="loading-text text-gray-500">加载中...</text>
...@@ -117,108 +133,44 @@ import { ref, computed, onMounted } from 'vue' ...@@ -117,108 +133,44 @@ import { ref, computed, onMounted } from 'vue'
117 import { Search2, Heart1, HeartFill } from '@nutui/icons-vue-taro' 133 import { Search2, Heart1, HeartFill } from '@nutui/icons-vue-taro'
118 import { useFavorite } from '@/composables/useFavorite' 134 import { useFavorite } from '@/composables/useFavorite'
119 import './index.less' 135 import './index.less'
136 +// 接口导入
137 +import { getVehicleBrandsAPI, getSchoolsAPI } from '@/api/other';
138 +import { getRecommendVehicleAPI } from '@/api/car';
139 +import { DEFAULT_COVER_IMG } from '@/utils/config'
120 140
121 // 响应式数据 141 // 响应式数据
122 const searchValue = ref('') 142 const searchValue = ref('')
123 -const onBlurSearch = () => { 143 +/**
124 - console.warn(searchValue.value) 144 + * 搜索框失焦事件
145 + */
146 +const onBlurSearch = async () => {
147 + // 重置分页并重新加载数据
148 + currentPage.value = 0
149 + hasMore.value = true
150 + await loadVehicleData()
125 } 151 }
126 // 收藏功能现在使用基于对象属性的模式 152 // 收藏功能现在使用基于对象属性的模式
127 153
128 // Filter states - 使用NutUI Menu组件 154 // Filter states - 使用NutUI Menu组件
129 -const selectedBrand = ref('全部品牌') 155 +const selectedBrand = ref('')
130 -const selectedYear = ref('出厂年份') 156 +const selectedYear = ref('')
131 -const selectedSchool = ref('所在学校') 157 +const selectedSchool = ref('')
132 158
133 // Menu选项数据 159 // Menu选项数据
134 -const brandOptions = ref([ 160 +const brandOptions = ref([])
135 - { text: '全部品牌', value: '全部品牌' }, 161 +
136 - { text: '雅迪', value: '雅迪' }, 162 +const yearOptions = ref([])
137 - { text: '台铃', value: '台铃' }, 163 +
138 - { text: '小鸟', value: '小鸟' }, 164 +const schoolOptions = ref([])
139 - { text: '新日', value: '新日' },
140 - { text: '爱玛', value: '爱玛' },
141 - { text: '小牛', value: '小牛' }
142 -])
143 -
144 -const yearOptions = ref([
145 - { text: '出厂年份', value: '出厂年份' },
146 - { text: '2024年', value: '2024年' },
147 - { text: '2023年', value: '2023年' },
148 - { text: '2022年', value: '2022年' },
149 - { text: '2021年', value: '2021年' },
150 - { text: '2020年', value: '2020年' }
151 -])
152 -
153 -const schoolOptions = ref([
154 - { text: '所在学校', value: '所在学校' },
155 - { text: '上海理工大学', value: '上海理工大学' },
156 - { text: '上海复旦大学', value: '上海复旦大学' },
157 - { text: '上海同济大学', value: '上海同济大学' },
158 - { text: '上海交通大学', value: '上海交通大学' }
159 -])
160 165
161 // 精品推荐车辆数据 166 // 精品推荐车辆数据
162 -const recommendCars = ref([ 167 +const recommendCars = ref([])
163 - {
164 - id: 1,
165 - name: '小牛NGT 电动车',
166 - year: '2024年',
167 - batteryHealth: 100,
168 - mileage: 0,
169 - price: 5200,
170 - school: '上海理工大学',
171 - imageUrl: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400&h=300&fit=crop',
172 - recommendReason: '品质优选',
173 - isNew: true,
174 - brand: '小牛'
175 - },
176 - {
177 - id: 2,
178 - name: '雅迪 DE3 电动车',
179 - year: '2024年',
180 - batteryHealth: 98,
181 - mileage: 200,
182 - price: 4800,
183 - school: '上海大学',
184 - imageUrl: 'https://images.unsplash.com/photo-1571068316344-75bc76f77890?w=400&h=300&fit=crop',
185 - recommendReason: '热门推荐',
186 - isNew: true,
187 - brand: '雅迪'
188 - },
189 - {
190 - id: 3,
191 - name: '爱玛 A600 电动车',
192 - year: '2024年',
193 - batteryHealth: 95,
194 - mileage: 500,
195 - price: 3800,
196 - school: '华东理工大学',
197 - imageUrl: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400&h=300&fit=crop',
198 - recommendReason: '性价比之选',
199 - isNew: true,
200 - brand: '爱玛'
201 - },
202 - {
203 - id: 4,
204 - name: '台铃 TDR-2024 电动车',
205 - year: '2024年',
206 - batteryHealth: 92,
207 - mileage: 800,
208 - price: 4200,
209 - school: '上海交通大学',
210 - imageUrl: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400&h=300&fit=crop',
211 - recommendReason: '口碑好评',
212 - isNew: true,
213 - brand: '台铃'
214 - }
215 -])
216 168
217 // 加载状态 169 // 加载状态
218 const loading = ref(false) 170 const loading = ref(false)
219 const hasMore = ref(true) 171 const hasMore = ref(true)
220 -const currentPage = ref(1) 172 +const currentPage = ref(0)
221 -const pageSize = ref(4) 173 +const pageSize = ref(10)
222 174
223 // Toast提示 175 // Toast提示
224 const toastVisible = ref(false) 176 const toastVisible = ref(false)
...@@ -284,79 +236,93 @@ const onSchoolChange = (value) => { ...@@ -284,79 +236,93 @@ const onSchoolChange = (value) => {
284 /** 236 /**
285 * 过滤车辆数据 237 * 过滤车辆数据
286 */ 238 */
287 -const filterCars = () => { 239 +const filterCars = async () => {
288 - // TODO: 实现过滤逻辑 240 + // 重置数据
241 + recommendCars.value = []
242 + currentPage.value = 0
243 + hasMore.value = true
244 +
245 + // 重新加载数据
246 + await loadVehicleData()
289 showToast('筛选条件已更新', 'success') 247 showToast('筛选条件已更新', 'success')
290 } 248 }
291 249
292 /** 250 /**
293 - * 生成模拟车辆数据 251 + * 加载车辆数据
294 - * @param {number} page - 页码 252 + * @param {boolean} isLoadMore - 是否为加载更多
295 - * @param {number} size - 每页数量
296 - * @returns {Array} 车辆数据数组
297 */ 253 */
298 -const generateMockData = (page, size) => { 254 +const loadVehicleData = async (isLoadMore = false) => {
299 - const brands = ['雅迪', '台铃', '小鸟', '新日', '爱玛', '小牛', '绿源', '立马'] 255 + if (loading.value) return
300 - const schools = ['上海理工大学', '上海复旦大学', '上海同济大学', '上海交通大学', '华东师范大学', '上海大学'] 256 +
301 - const years = ['2024年', '2023年', '2022年', '2021年', '2020年'] 257 + loading.value = true
302 - const images = [ 258 +
303 - 'https://images.unsplash.com/photo-1567922045116-2a00fae2ed03?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60', 259 + try {
304 - 'https://images.unsplash.com/photo-1573981368236-719bbb6f70f7?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60', 260 + // 构建请求参数
305 - 'https://images.unsplash.com/photo-1583568671741-c70dafa8e8e7?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60', 261 + const params = {
306 - 'https://images.unsplash.com/photo-1595941069915-4ebc5197c14a?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60', 262 + section: 3,
307 - 'https://images.unsplash.com/photo-1558981285-6f0c94958bb6?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60', 263 + page: currentPage.value,
308 - 'https://images.unsplash.com/photo-1558981403-c5f9899a28bc?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60' 264 + limit: pageSize.value
309 - ] 265 + }
310 - const recommendReasons = ['品质优选', '热门推荐', '性价比之选', '口碑好评', '精品车源', '优质推荐'] 266 +
311 - 267 + // 添加筛选条件
312 - const data = [] 268 + if (selectedSchool.value) {
313 - for (let i = 0; i < size; i++) { 269 + params.school_id = selectedSchool.value
314 - const index = (page - 1) * size + i 270 + }
315 - const brand = brands[Math.floor(Math.random() * brands.length)] 271 + if (selectedBrand.value) {
316 - const school = schools[Math.floor(Math.random() * schools.length)] 272 + params.brand = selectedBrand.value
317 - const year = years[Math.floor(Math.random() * years.length)] 273 + }
318 - const image = images[Math.floor(Math.random() * images.length)] 274 + if (selectedYear.value) {
319 - const recommendReason = recommendReasons[Math.floor(Math.random() * recommendReasons.length)] 275 + params.manufacture_year = selectedYear.value
320 - 276 + }
321 - data.push({ 277 + if (searchValue.value.trim()) {
322 - id: `recommend_${index + 100}`, 278 + params.keyword = searchValue.value.trim()
323 - name: `${brand} ${['豪华版', '标准版', '运动版', '经典版'][Math.floor(Math.random() * 4)]}`, 279 + }
324 - year: year, 280 +
325 - school: school, 281 + const response = await getRecommendVehicleAPI(params)
326 - price: Math.floor(Math.random() * 3000) + 3000, // 精品推荐价格相对较高 282 +
327 - imageUrl: image, 283 + if (response && response.code === 1 && response.data) {
328 - batteryHealth: Math.floor(Math.random() * 10) + 90, // 精品推荐电池健康度较高 284 + const vehicleList = response.data.list || []
329 - mileage: Math.floor(Math.random() * 1000), // 精品推荐里程较少 285 +
330 - brand: brand, 286 + // 处理图片数据
331 - recommendReason: recommendReason, 287 + const processedData = vehicleList.map(item => ({
332 - isNew: Math.random() > 0.3 // 70%概率显示推荐标签 288 + ...item,
333 - }) 289 + front_photo: item.front_photo || DEFAULT_COVER_IMG,
290 + // 添加推荐标签(精品推荐都显示推荐标签)
291 + isRecommend: true,
292 + // 确保价格为数字类型
293 + price: Number(item.price) || 0,
294 + market_price: Number(item.market_price) || 0
295 + }))
296 +
297 + if (isLoadMore) {
298 + recommendCars.value.push(...processedData)
299 + } else {
300 + recommendCars.value = processedData
301 + }
302 +
303 + // 检查是否还有更多数据 - 基于总数和当前已加载数量
304 + const totalLoaded = (currentPage.value + 1) * pageSize.value
305 + hasMore.value = totalLoaded < response.data.total
306 + } else {
307 + console.error('API返回错误:', response)
308 + showToast(response?.msg || '获取数据失败', 'error')
309 + }
310 + } catch (error) {
311 + console.error('加载车辆数据失败:', error)
312 + showToast('网络错误,请稍后重试', 'error')
313 + } finally {
314 + loading.value = false
334 } 315 }
335 - return data
336 } 316 }
337 317
338 /** 318 /**
339 * 加载更多数据 319 * 加载更多数据
340 */ 320 */
341 -const loadMore = () => { 321 +const loadMore = async () => {
342 if (loading.value || !hasMore.value) return 322 if (loading.value || !hasMore.value) return
343 323
344 - loading.value = true 324 + currentPage.value++
345 - 325 + await loadVehicleData(true)
346 - // 模拟网络请求延迟
347 - setTimeout(() => {
348 - // 模拟最多加载5页数据
349 - if (currentPage.value >= 5) {
350 - hasMore.value = false
351 - loading.value = false
352 - return
353 - }
354 -
355 - currentPage.value++
356 - const recommendData = generateMockData(currentPage.value, pageSize.value)
357 - recommendCars.value.push(...recommendData)
358 - loading.value = false
359 - }, 1000 + Math.random() * 1000)
360 } 326 }
361 327
362 /** 328 /**
...@@ -376,8 +342,43 @@ const showToast = (message, type = 'success') => { ...@@ -376,8 +342,43 @@ const showToast = (message, type = 'success') => {
376 } 342 }
377 343
378 // 初始化 344 // 初始化
379 -onMounted(() => { 345 +onMounted(async () => {
380 - // 可以在这里加载初始数据 346 + // 获取全部品牌数据
347 + const vBrands = await getVehicleBrandsAPI()
348 + if (vBrands.code) {
349 + brandOptions.value = vBrands.data.map(brand => ({
350 + text: brand,
351 + value: brand
352 + }))
353 + brandOptions.value = [{
354 + text: '全部品牌',
355 + value: ''
356 + }, ...brandOptions.value]
357 + }
358 + // 生成从当前日期开始往前10年的年份
359 + yearOptions.value = Array.from({ length: 10 }, (_, i) => ({
360 + text: (new Date().getFullYear() - i).toString() + '年',
361 + value: (new Date().getFullYear() - i).toString()
362 + }))
363 + yearOptions.value = [{
364 + text: '全部年份',
365 + value: ''
366 + }, ...yearOptions.value]
367 + // 获取全部学校数据
368 + const schoolData = await getSchoolsAPI()
369 + if (schoolData.code) {
370 + schoolOptions.value = schoolData.data.map(school => ({
371 + text: school.name,
372 + value: school.id
373 + }))
374 + }
375 + schoolOptions.value = [{
376 + text: '全部学校',
377 + value: ''
378 + }, ...schoolOptions.value]
379 +
380 + // 加载初始车辆数据
381 + await loadVehicleData()
381 }) 382 })
382 </script> 383 </script>
383 384
......