feat(推荐车辆列表): 实现推荐车辆列表接口对接及UI优化
- 对接获取推荐车辆、品牌和学校接口 - 替换静态数据为接口返回的动态数据 - 优化车辆卡片UI显示,增加原价显示和推荐标签 - 添加空状态显示和加载更多功能 - 实现筛选条件和搜索功能
Showing
1 changed file
with
164 additions
and
163 deletions
| ... | @@ -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 | ... | ... |
-
Please register or login to post a comment