hookehuyr

feat(特价好车): 实现特价好车页面数据对接API

- 移除模拟数据,改为从API获取真实车辆数据
- 实现品牌、年份、学校的筛选功能
- 添加分页加载更多功能
- 更新车辆卡片显示字段以匹配API返回数据
1 /* 1 /*
2 * @Date: 2025-07-09 14:58:51 2 * @Date: 2025-07-09 14:58:51
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-07-10 14:45:49 4 + * @LastEditTime: 2025-07-11 10:18:35
5 * @FilePath: /jgdl/src/api/car.js 5 * @FilePath: /jgdl/src/api/car.js
6 * @Description: 车辆相关API接口 6 * @Description: 车辆相关API接口
7 */ 7 */
...@@ -66,6 +66,13 @@ export const editVehicleAPI = (params) => fn(fetch.post(Api.EDIT_VEHICLE, params ...@@ -66,6 +66,13 @@ export const editVehicleAPI = (params) => fn(fetch.post(Api.EDIT_VEHICLE, params
66 /** 66 /**
67 * @description: 首页轮播/最新上架/特价好车 67 * @description: 首页轮播/最新上架/特价好车
68 * @param section 推荐的位置,1=首页轮播, 2=特价好车, 3=精品推荐 68 * @param section 推荐的位置,1=首页轮播, 2=特价好车, 3=精品推荐
69 + * @param school_id 学校ID
70 + * @param brand 品牌
71 + * @param manufacture_year 出厂年份
72 + * @param verification_status 认证状态(1=不认证, 3=认证待审核, 5=已认证, 7=认证失败)
73 + * @param keyword 搜索关键字(品牌/型号/描述)
74 + * @param page 页码
75 + * @param limit 每页数量
69 * @returns data[{ id, seller_id, school_id, school_name, title, brand, model, manufacture_year, new_level, range_km, total_mileage_km, max_speed_kmh, battery_capacity_ah, brake_wear_level, tire_wear_level, price, market_price, verification_status, rejection_reason, note, photos, is_favorite }] 76 * @returns data[{ id, seller_id, school_id, school_name, title, brand, model, manufacture_year, new_level, range_km, total_mileage_km, max_speed_kmh, battery_capacity_ah, brake_wear_level, tire_wear_level, price, market_price, verification_status, rejection_reason, note, photos, is_favorite }]
70 */ 77 */
71 78
......
1 <!-- 1 <!--
2 * @Date: 2022-09-19 14:11:06 2 * @Date: 2022-09-19 14:11:06
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-07-04 16:58:46 4 + * @LastEditTime: 2025-07-11 10:45:41
5 * @FilePath: /jgdl/src/pages/goodCarList/index.vue 5 * @FilePath: /jgdl/src/pages/goodCarList/index.vue
6 * @Description: 特价好车页面 6 * @Description: 特价好车页面
7 --> 7 -->
...@@ -53,45 +53,45 @@ ...@@ -53,45 +53,45 @@
53 > 53 >
54 <view class="flex"> 54 <view class="flex">
55 <view class="w-32 h-24 relative p-2"> 55 <view class="w-32 h-24 relative p-2">
56 - <image :src="car.imageUrl" :alt="car.name" mode="aspectFill" 56 + <image :src="car.front_photo" :alt="car.name" mode="aspectFill"
57 class="w-full h-full object-cover rounded-lg" /> 57 class="w-full h-full object-cover rounded-lg" />
58 <view v-if="car.isSpecial" 58 <view v-if="car.isSpecial"
59 class="absolute bottom-3 right-3 bg-red-500 text-white text-xs px-1 rounded flex items-center"> 59 class="absolute bottom-3 right-3 bg-red-500 text-white text-xs px-1 rounded flex items-center">
60 <text class="text-white">特</text> 60 <text class="text-white">特</text>
61 </view> 61 </view>
62 <!-- 折扣标签 --> 62 <!-- 折扣标签 -->
63 - <view v-if="car.discount" 63 + <!-- <view v-if="car.discount"
64 class="absolute top-3 left-3 bg-red-500 text-white text-xs px-1 rounded"> 64 class="absolute top-3 left-3 bg-red-500 text-white text-xs px-1 rounded">
65 <text class="text-white">{{ car.discount }}折</text> 65 <text class="text-white">{{ car.discount }}折</text>
66 - </view> 66 + </view> -->
67 </view> 67 </view>
68 <view class="flex-1 p-3 relative"> 68 <view class="flex-1 p-3 relative">
69 <view class="absolute top-3 right-4" @tap.stop="() => toggleFavorite(car)"> 69 <view class="absolute top-3 right-4" @tap.stop="() => toggleFavorite(car)">
70 <Heart1 v-if="!car.is_favorite" size="22" color="#9ca3af" /> 70 <Heart1 v-if="!car.is_favorite" size="22" color="#9ca3af" />
71 <HeartFill v-else size="22" color="#ef4444" /> 71 <HeartFill v-else size="22" color="#ef4444" />
72 </view> 72 </view>
73 - <text class="font-medium text-sm block">{{ car.name }}</text> 73 + <text class="font-medium text-sm block">{{ car.brand }} {{ car.model }}</text>
74 <text class="text-xs text-gray-600 mt-1 block"> 74 <text class="text-xs text-gray-600 mt-1 block">
75 - {{ car.year }} · 75 + {{ car.manufacture_year }} ·
76 - <text v-if="car.batteryHealth">电池健康度{{ car.batteryHealth }}%</text> 76 + <text v-if="car.range_km">续航{{ car.range_km }}km</text>
77 - <text v-if="car.mileage"> 行驶{{ car.mileage }}公里</text> 77 + <text v-if="car.max_speed_kmh"> 最高时速{{ car.max_speed_kmh }}km/h</text>
78 </text> 78 </text>
79 <view class="mt-2"> 79 <view class="mt-2">
80 <!-- 原价和现价 --> 80 <!-- 原价和现价 -->
81 <view class="flex items-center"> 81 <view class="flex items-center">
82 - <text v-if="car.originalPrice" class="text-xs text-gray-400 line-through mr-2"> 82 + <text v-if="car.market_price" class="text-xs text-gray-400 line-through mr-2">
83 - ¥{{ car.originalPrice.toLocaleString() }} 83 + ¥{{ car.market_price.toLocaleString() }}
84 </text> 84 </text>
85 <text class="text-orange-500 font-bold" style="font-size: 1.2rem;"> 85 <text class="text-orange-500 font-bold" style="font-size: 1.2rem;">
86 ¥{{ car.price.toLocaleString() }} 86 ¥{{ car.price.toLocaleString() }}
87 </text> 87 </text>
88 </view> 88 </view>
89 - <text class="text-xs text-gray-500 mt-1 block">{{ car.school }}</text> 89 + <text class="text-xs text-gray-500 mt-1 block">{{ car.school_name }}</text>
90 </view> 90 </view>
91 <!-- 特价标签 --> 91 <!-- 特价标签 -->
92 - <view class="mt-1"> 92 + <!-- <view class="mt-1">
93 <text class="text-xs text-red-600">{{ car.specialTag }}</text> 93 <text class="text-xs text-red-600">{{ car.specialTag }}</text>
94 - </view> 94 + </view> -->
95 </view> 95 </view>
96 </view> 96 </view>
97 </view> 97 </view>
...@@ -128,116 +128,44 @@ import { ref, computed, onMounted } from 'vue' ...@@ -128,116 +128,44 @@ import { ref, computed, onMounted } from 'vue'
128 import { Search2, Heart1, HeartFill } from '@nutui/icons-vue-taro' 128 import { Search2, Heart1, HeartFill } from '@nutui/icons-vue-taro'
129 import { useFavorite } from '@/composables/useFavorite' 129 import { useFavorite } from '@/composables/useFavorite'
130 import './index.less' 130 import './index.less'
131 +// 接口导入
132 +import { getVehicleBrandsAPI, getSchoolsAPI } from '@/api/other';
133 +import { getRecommendVehicleAPI } from '@/api/car';
134 +import { DEFAULT_COVER_IMG } from '@/utils/config'
131 135
132 // 响应式数据 136 // 响应式数据
133 const searchValue = ref('') 137 const searchValue = ref('')
134 -const onBlurSearch = () => { 138 +/**
135 - console.warn(searchValue.value) 139 + * 搜索框失焦事件
140 + */
141 +const onBlurSearch = async () => {
142 + // 重置分页并重新加载数据
143 + currentPage.value = 0
144 + hasMore.value = true
145 + await loadVehicleData()
136 } 146 }
137 // 收藏功能现在使用基于对象属性的模式 147 // 收藏功能现在使用基于对象属性的模式
138 148
139 // Filter states - 使用NutUI Menu组件 149 // Filter states - 使用NutUI Menu组件
140 -const selectedBrand = ref('全部品牌') 150 +const selectedBrand = ref('')
141 -const selectedYear = ref('出厂年份') 151 +const selectedYear = ref('')
142 -const selectedSchool = ref('所在学校') 152 +const selectedSchool = ref('')
143 153
144 // Menu选项数据 154 // Menu选项数据
145 -const brandOptions = ref([ 155 +const brandOptions = ref([])
146 - { text: '全部品牌', value: '全部品牌' }, 156 +
147 - { text: '雅迪', value: '雅迪' }, 157 +const yearOptions = ref([])
148 - { text: '台铃', value: '台铃' }, 158 +
149 - { text: '小鸟', value: '小鸟' }, 159 +const schoolOptions = ref([])
150 - { text: '新日', value: '新日' },
151 - { text: '爱玛', value: '爱玛' },
152 - { text: '小牛', value: '小牛' }
153 -])
154 -
155 -const yearOptions = ref([
156 - { text: '出厂年份', value: '出厂年份' },
157 - { text: '2024年', value: '2024年' },
158 - { text: '2023年', value: '2023年' },
159 - { text: '2022年', value: '2022年' },
160 - { text: '2021年', value: '2021年' },
161 - { text: '2020年', value: '2020年' }
162 -])
163 -
164 -const schoolOptions = ref([
165 - { text: '所在学校', value: '所在学校' },
166 - { text: '上海理工大学', value: '上海理工大学' },
167 - { text: '上海复旦大学', value: '上海复旦大学' },
168 - { text: '上海同济大学', value: '上海同济大学' },
169 - { text: '上海交通大学', value: '上海交通大学' }
170 -])
171 160
172 // 特价好车数据 161 // 特价好车数据
173 -const goodCars = ref([ 162 +const goodCars = ref([])
174 - {
175 - id: 1,
176 - name: '雅迪 DE3 电动车',
177 - year: '2023年',
178 - batteryHealth: 85,
179 - mileage: 1200,
180 - originalPrice: 4800,
181 - price: 3600,
182 - discount: 7.5,
183 - school: '上海理工大学',
184 - imageUrl: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400&h=300&fit=crop',
185 - specialTag: '限时特价',
186 - isSpecial: true,
187 - brand: '雅迪'
188 - },
189 - {
190 - id: 2,
191 - name: '爱玛 A600 电动车',
192 - year: '2022年',
193 - batteryHealth: 80,
194 - mileage: 2000,
195 - originalPrice: 3800,
196 - price: 2850,
197 - discount: 7.5,
198 - school: '上海大学',
199 - imageUrl: 'https://images.unsplash.com/photo-1571068316344-75bc76f77890?w=400&h=300&fit=crop',
200 - specialTag: '急售特价',
201 - isSpecial: true,
202 - brand: '爱玛'
203 - },
204 - {
205 - id: 3,
206 - name: '台铃 TDR-2023 电动车',
207 - year: '2023年',
208 - batteryHealth: 88,
209 - mileage: 800,
210 - originalPrice: 4200,
211 - price: 3360,
212 - discount: 8.0,
213 - school: '华东理工大学',
214 - imageUrl: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400&h=300&fit=crop',
215 - specialTag: '毕业甩卖',
216 - isSpecial: true,
217 - brand: '台铃'
218 - },
219 - {
220 - id: 4,
221 - name: '小牛 NGT 电动车',
222 - year: '2022年',
223 - batteryHealth: 75,
224 - mileage: 3000,
225 - originalPrice: 5200,
226 - price: 3640,
227 - discount: 7.0,
228 - school: '上海交通大学',
229 - imageUrl: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400&h=300&fit=crop',
230 - specialTag: '超值特价',
231 - isSpecial: true,
232 - brand: '小牛'
233 - }
234 -])
235 163
236 // 加载状态 164 // 加载状态
237 const loading = ref(false) 165 const loading = ref(false)
238 const hasMore = ref(true) 166 const hasMore = ref(true)
239 -const currentPage = ref(1) 167 +const currentPage = ref(0)
240 -const pageSize = ref(4) 168 +const pageSize = ref(10)
241 169
242 // Toast提示 170 // Toast提示
243 const toastVisible = ref(false) 171 const toastVisible = ref(false)
...@@ -303,84 +231,93 @@ const onSchoolChange = (value) => { ...@@ -303,84 +231,93 @@ const onSchoolChange = (value) => {
303 /** 231 /**
304 * 过滤车辆数据 232 * 过滤车辆数据
305 */ 233 */
306 -const filterCars = () => { 234 +const filterCars = async () => {
307 - // TODO: 实现过滤逻辑 235 + // 重置数据
236 + goodCars.value = []
237 + currentPage.value = 0
238 + hasMore.value = true
239 +
240 + // 重新加载数据
241 + await loadVehicleData()
308 showToast('筛选条件已更新', 'success') 242 showToast('筛选条件已更新', 'success')
309 } 243 }
310 244
311 /** 245 /**
312 - * 生成模拟车辆数据 246 + * 加载车辆数据
313 - * @param {number} page - 页码 247 + * @param {boolean} isLoadMore - 是否为加载更多
314 - * @param {number} size - 每页数量
315 - * @returns {Array} 车辆数据数组
316 */ 248 */
317 -const generateMockData = (page, size) => { 249 +const loadVehicleData = async (isLoadMore = false) => {
318 - const brands = ['雅迪', '台铃', '小鸟', '新日', '爱玛', '小牛', '绿源', '立马'] 250 + if (loading.value) return
319 - const schools = ['上海理工大学', '上海复旦大学', '上海同济大学', '上海交通大学', '华东师范大学', '上海大学'] 251 +
320 - const years = ['2024年', '2023年', '2022年', '2021年', '2020年'] 252 + loading.value = true
321 - const images = [ 253 +
322 - 'https://images.unsplash.com/photo-1567922045116-2a00fae2ed03?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60', 254 + try {
323 - 'https://images.unsplash.com/photo-1573981368236-719bbb6f70f7?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60', 255 + // 构建请求参数
324 - 'https://images.unsplash.com/photo-1583568671741-c70dafa8e8e7?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60', 256 + const params = {
325 - 'https://images.unsplash.com/photo-1595941069915-4ebc5197c14a?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60', 257 + section: 2,
326 - 'https://images.unsplash.com/photo-1558981285-6f0c94958bb6?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60', 258 + page: currentPage.value,
327 - 'https://images.unsplash.com/photo-1558981403-c5f9899a28bc?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60' 259 + limit: pageSize.value
328 - ] 260 + }
329 - const specialTags = ['限时特价', '急售特价', '毕业甩卖', '超值特价', '清仓特价', '年底促销'] 261 +
330 - 262 + // 添加筛选条件
331 - const data = [] 263 + if (selectedSchool.value) {
332 - for (let i = 0; i < size; i++) { 264 + params.school_id = selectedSchool.value
333 - const index = (page - 1) * size + i 265 + }
334 - const brand = brands[Math.floor(Math.random() * brands.length)] 266 + if (selectedBrand.value) {
335 - const school = schools[Math.floor(Math.random() * schools.length)] 267 + params.brand = selectedBrand.value
336 - const year = years[Math.floor(Math.random() * years.length)] 268 + }
337 - const image = images[Math.floor(Math.random() * images.length)] 269 + if (selectedYear.value) {
338 - const specialTag = specialTags[Math.floor(Math.random() * specialTags.length)] 270 + params.manufacture_year = selectedYear.value
339 - const originalPrice = Math.floor(Math.random() * 2000) + 3000 271 + }
340 - const discount = (Math.floor(Math.random() * 30) + 60) / 10 // 6.0-8.9折 272 + if (searchValue.value.trim()) {
341 - const price = Math.floor(originalPrice * discount / 10) 273 + params.keyword = searchValue.value.trim()
342 - 274 + }
343 - data.push({ 275 +
344 - id: `good_${index + 100}`, 276 + const response = await getRecommendVehicleAPI(params)
345 - name: `${brand} ${['豪华版', '标准版', '运动版', '经典版'][Math.floor(Math.random() * 4)]}`, 277 +
346 - year: year, 278 + if (response && response.code === 1 && response.data) {
347 - school: school, 279 + const vehicleList = response.data.list || []
348 - originalPrice: originalPrice, 280 +
349 - price: price, 281 + // 处理图片数据
350 - discount: discount, 282 + const processedData = vehicleList.map(item => ({
351 - imageUrl: image, 283 + ...item,
352 - batteryHealth: Math.floor(Math.random() * 30) + 70, // 特价车电池健康度相对较低 284 + front_photo: item.front_photo || DEFAULT_COVER_IMG,
353 - mileage: Math.floor(Math.random() * 3000) + 1000, // 特价车里程相对较高 285 + // 添加特价标签(有市场价且当前价格低于市场价的车辆)
354 - brand: brand, 286 + isSpecial: item.market_price && item.price < item.market_price,
355 - specialTag: specialTag, 287 + // 确保价格为数字类型
356 - isSpecial: true 288 + price: Number(item.price) || 0,
357 - }) 289 + market_price: Number(item.market_price) || 0
290 + }))
291 +
292 + if (isLoadMore) {
293 + goodCars.value.push(...processedData)
294 + } else {
295 + goodCars.value = processedData
296 + }
297 +
298 + // 检查是否还有更多数据 - 基于总数和当前已加载数量
299 + const totalLoaded = (currentPage.value + 1) * pageSize.value
300 + hasMore.value = totalLoaded < response.data.total
301 + } else {
302 + console.error('API返回错误:', response)
303 + showToast(response?.msg || '获取数据失败', 'error')
304 + }
305 + } catch (error) {
306 + console.error('加载车辆数据失败:', error)
307 + showToast('网络错误,请稍后重试', 'error')
308 + } finally {
309 + loading.value = false
358 } 310 }
359 - return data
360 } 311 }
361 312
362 /** 313 /**
363 * 加载更多数据 314 * 加载更多数据
364 */ 315 */
365 -const loadMore = () => { 316 +const loadMore = async () => {
366 if (loading.value || !hasMore.value) return 317 if (loading.value || !hasMore.value) return
367 318
368 - loading.value = true 319 + currentPage.value++
369 - 320 + await loadVehicleData(true)
370 - // 模拟网络请求延迟
371 - setTimeout(() => {
372 - // 模拟最多加载5页数据
373 - if (currentPage.value >= 5) {
374 - hasMore.value = false
375 - loading.value = false
376 - return
377 - }
378 -
379 - currentPage.value++
380 - const newData = generateMockData(currentPage.value, pageSize.value)
381 - goodCars.value.push(...newData)
382 - loading.value = false
383 - }, 1000 + Math.random() * 1000)
384 } 321 }
385 322
386 /** 323 /**
...@@ -400,8 +337,43 @@ const showToast = (message, type = 'success') => { ...@@ -400,8 +337,43 @@ const showToast = (message, type = 'success') => {
400 } 337 }
401 338
402 // 初始化 339 // 初始化
403 -onMounted(() => { 340 +onMounted(async () => {
404 - // 可以在这里加载初始数据 341 + // 获取全部品牌数据
342 + const vBrands = await getVehicleBrandsAPI()
343 + if (vBrands.code) {
344 + brandOptions.value = vBrands.data.map(brand => ({
345 + text: brand,
346 + value: brand
347 + }))
348 + brandOptions.value = [{
349 + text: '全部品牌',
350 + value: ''
351 + }, ...brandOptions.value]
352 + }
353 + // 生成从当前日期开始往前10年的年份
354 + yearOptions.value = Array.from({ length: 10 }, (_, i) => ({
355 + text: (new Date().getFullYear() - i).toString() + '年',
356 + value: (new Date().getFullYear() - i).toString()
357 + }))
358 + yearOptions.value = [{
359 + text: '全部年份',
360 + value: ''
361 + }, ...yearOptions.value]
362 + // 获取全部学校数据
363 + const schoolData = await getSchoolsAPI()
364 + if (schoolData.code) {
365 + schoolOptions.value = schoolData.data.map(school => ({
366 + text: school.name,
367 + value: school.id
368 + }))
369 + }
370 + schoolOptions.value = [{
371 + text: '全部学校',
372 + value: ''
373 + }, ...schoolOptions.value]
374 +
375 + // 加载初始车辆数据
376 + await loadVehicleData()
405 }) 377 })
406 </script> 378 </script>
407 379
......
1 <!-- 1 <!--
2 * @Date: 2025-06-28 10:33:00 2 * @Date: 2025-06-28 10:33:00
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-07-11 10:09:02 4 + * @LastEditTime: 2025-07-11 10:58:14
5 * @FilePath: /jgdl/src/pages/index/index.vue 5 * @FilePath: /jgdl/src/pages/index/index.vue
6 * @Description: 捡个电驴首页 6 * @Description: 捡个电驴首页
7 --> 7 -->
...@@ -290,14 +290,14 @@ onMounted(async () => { ...@@ -290,14 +290,14 @@ onMounted(async () => {
290 // 获取首页轮播 290 // 获取首页轮播
291 const res1 = await getRecommendVehicleAPI({ section: 1 }) 291 const res1 = await getRecommendVehicleAPI({ section: 1 })
292 if (res1.code) { 292 if (res1.code) {
293 - bannerImages.value = res1.data.map(item => item.front_photo); 293 + bannerImages.value = res1.data.list.map(item => item.front_photo);
294 } 294 }
295 // 获取精品推荐 295 // 获取精品推荐
296 const res2 = await getRecommendVehicleAPI({ section: 3 }) 296 const res2 = await getRecommendVehicleAPI({ section: 3 })
297 if (res2.code) { 297 if (res2.code) {
298 - featuredScooters.value = res2.data 298 + featuredScooters.value = res2.data.list
299 // 处理图片数据 299 // 处理图片数据
300 - const processedData = res2.data.map(item => ({ 300 + const processedData = res2.data.list.map(item => ({
301 ...item, 301 ...item,
302 front_photo: item.front_photo || DEFAULT_COVER_IMG, 302 front_photo: item.front_photo || DEFAULT_COVER_IMG,
303 // 确保价格为数字类型 303 // 确保价格为数字类型
......