hookehuyr

feat(车辆列表): 重构车辆列表页面使用接口数据并添加筛选功能

- 替换模拟数据为真实接口数据
- 添加品牌、年份和学校筛选功能
- 实现分页加载更多功能
- 添加加载状态和提示信息
...@@ -34,9 +34,9 @@ ...@@ -34,9 +34,9 @@
34 class="bg-white rounded-lg shadow-sm overflow-hidden mb-3" @tap="() => onProductClick(scooter)"> 34 class="bg-white rounded-lg shadow-sm overflow-hidden mb-3" @tap="() => onProductClick(scooter)">
35 <view class="flex"> 35 <view class="flex">
36 <view class="w-32 h-24 relative p-2"> 36 <view class="w-32 h-24 relative p-2">
37 - <image :src="scooter.imageUrl" :alt="scooter.name" mode="aspectFill" 37 + <image :src="scooter.front_photo" mode="aspectFill"
38 class="w-full h-full object-cover rounded-lg" /> 38 class="w-full h-full object-cover rounded-lg" />
39 - <view v-if="scooter.isVerified" 39 + <view v-if="scooter.verification_status === 5"
40 class="absolute bottom-3 right-3 bg-orange-500 text-white text-xs px-1 rounded flex items-center"> 40 class="absolute bottom-3 right-3 bg-orange-500 text-white text-xs px-1 rounded flex items-center">
41 <Check size="12" color="#ffffff" class="mr-0.5" /> 41 <Check size="12" color="#ffffff" class="mr-0.5" />
42 <text class="text-white">认证</text> 42 <text class="text-white">认证</text>
...@@ -47,20 +47,20 @@ ...@@ -47,20 +47,20 @@
47 <Heart1 v-if="!scooter.is_favorite" size="22" color="#9ca3af" /> 47 <Heart1 v-if="!scooter.is_favorite" size="22" color="#9ca3af" />
48 <HeartFill v-else size="22" color="#ef4444" /> 48 <HeartFill v-else size="22" color="#ef4444" />
49 </view> 49 </view>
50 - <text class="font-medium text-sm block">{{ scooter.name }}</text> 50 + <text class="font-medium text-sm block">{{ scooter.brand }} {{ scooter.model }}</text>
51 <text class="text-xs text-gray-600 mt-1 block"> 51 <text class="text-xs text-gray-600 mt-1 block">
52 - {{ scooter.year }} · 52 + {{ scooter.manufacture_year }} ·
53 - <text v-if="scooter.batteryHealth">电池健康度{{ scooter.batteryHealth }}%</text> 53 + <text v-if="scooter.range_km">续航{{ scooter.range_km }}km</text>
54 - <text v-if="scooter.mileage"> 行驶{{ scooter.mileage }}公里</text> 54 + <text v-if="scooter.max_speed_kmh"> 最高时速{{ scooter.max_speed_kmh }}km/h</text>
55 </text> 55 </text>
56 <view class="mt-2"> 56 <view class="mt-2">
57 <text class="text-orange-500 font-bold" style="font-size: 1.2rem;"> 57 <text class="text-orange-500 font-bold" style="font-size: 1.2rem;">
58 ¥{{ scooter.price.toLocaleString() }} 58 ¥{{ scooter.price.toLocaleString() }}
59 </text> 59 </text>
60 - <text v-if="scooter.isVerified" class="ml-2 text-xs px-1 py-0.5 bg-orange-100 text-orange-500 rounded"> 60 + <!-- <text v-if="scooter.isVerified" class="ml-2 text-xs px-1 py-0.5 bg-orange-100 text-orange-500 rounded">
61 低于市场价 10% 61 低于市场价 10%
62 - </text> 62 + </text> -->
63 - <text class="text-xs text-gray-500 mt-1 block">{{ scooter.school }}</text> 63 + <text class="text-xs text-gray-500 mt-1 block">{{ scooter.school_name }}</text>
64 </view> 64 </view>
65 </view> 65 </view>
66 </view> 66 </view>
...@@ -74,11 +74,11 @@ ...@@ -74,11 +74,11 @@
74 <text class="loading-text">加载中...</text> 74 <text class="loading-text">加载中...</text>
75 </view> 75 </view>
76 76
77 - <view v-else-if="noMoreData" class="no-more-data"> 77 + <view v-else-if="!hasMore && scooters.length > 0" class="no-more-data">
78 <text>没有更多数据了</text> 78 <text>没有更多数据了</text>
79 </view> 79 </view>
80 80
81 - <nut-button v-else shape="round" type="default" plain @click="loadMoreData">点击加载更多</nut-button> 81 + <nut-button v-else-if="hasMore" shape="round" type="default" plain @click="loadMoreData">点击加载更多</nut-button>
82 </view> 82 </view>
83 </view> 83 </view>
84 84
...@@ -90,112 +90,64 @@ ...@@ -90,112 +90,64 @@
90 90
91 <!-- 自定义TabBar --> 91 <!-- 自定义TabBar -->
92 <TabBar /> 92 <TabBar />
93 +
94 + <!-- 成功提示 -->
95 + <nut-toast
96 + v-model:visible="toastVisible"
97 + :msg="toastMessage"
98 + :type="toastType"
99 + />
93 </view> 100 </view>
94 </template> 101 </template>
95 102
96 <script setup> 103 <script setup>
97 -import { ref } from 'vue' 104 +import { ref, onMounted } from 'vue'
98 import Taro from '@tarojs/taro' 105 import Taro from '@tarojs/taro'
99 import { Search2, Check, Heart1, HeartFill } from '@nutui/icons-vue-taro' 106 import { Search2, Check, Heart1, HeartFill } from '@nutui/icons-vue-taro'
100 import TabBar from '@/components/TabBar.vue' 107 import TabBar from '@/components/TabBar.vue'
101 import FeaturedRecommendations from '@/components/FeaturedRecommendations.vue' 108 import FeaturedRecommendations from '@/components/FeaturedRecommendations.vue'
102 import { useFavorite } from '@/composables/useFavorite' 109 import { useFavorite } from '@/composables/useFavorite'
110 +// 接口导入
111 +import { getVehicleBrandsAPI, getSchoolsAPI } from '@/api/other';
112 +import { getVehicleListAPI } from '@/api/car';
113 +import { DEFAULT_COVER_IMG } from '@/utils/config'
103 114
104 // 响应式数据 115 // 响应式数据
105 const searchValue = ref('') 116 const searchValue = ref('')
106 -const onBlurSearch = () => { 117 +/**
107 - console.warn(searchValue.value) 118 + * 搜索框失焦事件
119 + */
120 +const onBlurSearch = async () => {
121 + // 重置分页并重新加载数据
122 + currentPage.value = 0
123 + hasMore.value = true
124 + await loadVehicleData()
108 } 125 }
109 // 收藏功能现在使用基于对象属性的模式 126 // 收藏功能现在使用基于对象属性的模式
110 127
111 -// 无限滚动相关状态 128 +// 分页相关状态
112 const loading = ref(false) 129 const loading = ref(false)
113 -const noMoreData = ref(false) 130 +const hasMore = ref(true)
114 -const currentPage = ref(1) 131 +const currentPage = ref(0)
115 -const pageSize = ref(4) 132 +const pageSize = ref(10)
133 +
134 +// Toast提示
135 +const toastVisible = ref(false)
136 +const toastMessage = ref('')
137 +const toastType = ref('success')
116 138
117 // Filter states - 使用NutUI Menu组件 139 // Filter states - 使用NutUI Menu组件
118 -const selectedBrand = ref('全部品牌') 140 +const selectedBrand = ref('')
119 -const selectedYear = ref('出厂年份') 141 +const selectedYear = ref('')
120 -const selectedSchool = ref('所在学校') 142 +const selectedSchool = ref('')
121 143
122 // Menu选项数据 144 // Menu选项数据
123 -const brandOptions = ref([ 145 +const brandOptions = ref([])
124 - { text: '全部品牌', value: '全部品牌' }, 146 +const yearOptions = ref([])
125 - { text: '雅迪', value: '雅迪' }, 147 +const schoolOptions = ref([])
126 - { text: '台铃', value: '台铃' }, 148 +
127 - { text: '小鸟', value: '小鸟' }, 149 +// 车辆列表数据
128 - { text: '新日', value: '新日' }, 150 +const scooters = ref([])
129 - { text: '爱玛', value: '爱玛' },
130 - { text: '小牛', value: '小牛' }
131 -])
132 -
133 -const yearOptions = ref([
134 - { text: '出厂年份', value: '出厂年份' },
135 - { text: '2024年', value: '2024年' },
136 - { text: '2023年', value: '2023年' },
137 - { text: '2022年', value: '2022年' },
138 - { text: '2021年', value: '2021年' },
139 - { text: '2020年', value: '2020年' }
140 -])
141 -
142 -const schoolOptions = ref([
143 - { text: '所在学校', value: '所在学校' },
144 - { text: '上海理工大学', value: '上海理工大学' },
145 - { text: '上海复旦大学', value: '上海复旦大学' },
146 - { text: '上海同济大学', value: '上海同济大学' },
147 - { text: '上海交通大学', value: '上海交通大学' }
148 -])
149 -
150 -// 主要车辆列表数据 - 参考PostPage.tsx
151 -const scooters = ref([
152 - {
153 - id: '5',
154 - name: '雅迪 豪华版',
155 - year: '2023年',
156 - school: '上海理工大学',
157 - price: 3200,
158 - imageUrl: 'https://images.unsplash.com/photo-1567922045116-2a00fae2ed03?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60',
159 - batteryHealth: 98,
160 - mileage: 1200,
161 - brand: '雅迪',
162 - isVerified: true
163 - },
164 - {
165 - id: '6',
166 - name: '台铃 战速',
167 - year: '2022年',
168 - school: '上海理工大学',
169 - price: 3800,
170 - imageUrl: 'https://images.unsplash.com/photo-1567922045116-2a00fae2ed03?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60',
171 - batteryHealth: 92,
172 - mileage: 2500,
173 - brand: '台铃'
174 - },
175 - {
176 - id: '7',
177 - name: '小鸟电车',
178 - year: '2023年',
179 - school: '上海复旦大学',
180 - price: 3100,
181 - imageUrl: 'https://images.unsplash.com/photo-1567922045116-2a00fae2ed03?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60',
182 - batteryHealth: 92,
183 - mileage: 2000,
184 - brand: '小鸟'
185 - },
186 - {
187 - id: '8',
188 - name: '新日电动车',
189 - year: '2024年',
190 - school: '上海同济大学',
191 - price: 6700,
192 - imageUrl: 'https://images.unsplash.com/photo-1595941069915-4ebc5197c14a?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60',
193 - batteryHealth: 96,
194 - mileage: 500,
195 - brand: '新日',
196 - isVerified: true
197 - }
198 -])
199 151
200 152
201 153
...@@ -225,7 +177,7 @@ const onProductClick = (scooter) => { ...@@ -225,7 +177,7 @@ const onProductClick = (scooter) => {
225 */ 177 */
226 const onBrandChange = (value) => { 178 const onBrandChange = (value) => {
227 selectedBrand.value = value 179 selectedBrand.value = value
228 - // 这里可以添加过滤逻辑 180 + filterCars()
229 } 181 }
230 182
231 /** 183 /**
...@@ -234,7 +186,7 @@ const onBrandChange = (value) => { ...@@ -234,7 +186,7 @@ const onBrandChange = (value) => {
234 */ 186 */
235 const onYearChange = (value) => { 187 const onYearChange = (value) => {
236 selectedYear.value = value 188 selectedYear.value = value
237 - // 这里可以添加过滤逻辑 189 + filterCars()
238 } 190 }
239 191
240 /** 192 /**
...@@ -243,84 +195,143 @@ const onYearChange = (value) => { ...@@ -243,84 +195,143 @@ const onYearChange = (value) => {
243 */ 195 */
244 const onSchoolChange = (value) => { 196 const onSchoolChange = (value) => {
245 selectedSchool.value = value 197 selectedSchool.value = value
246 - // 这里可以添加过滤逻辑 198 + filterCars()
247 } 199 }
248 200
249 /** 201 /**
250 - * 生成模拟车辆数据 202 + * 过滤车辆数据
251 - * @param {number} page - 页码
252 - * @param {number} size - 每页数量
253 - * @returns {Array} 车辆数据数组
254 */ 203 */
255 -const generateMockData = (page, size) => { 204 +const filterCars = async () => {
256 - const brands = ['雅迪', '台铃', '小鸟', '新日', '爱玛', '小牛', '绿源', '立马'] 205 + // 重置数据
257 - const schools = ['上海理工大学', '上海复旦大学', '上海同济大学', '上海交通大学', '华东师范大学', '上海大学'] 206 + scooters.value = []
258 - const years = ['2024年', '2023年', '2022年', '2021年', '2020年'] 207 + currentPage.value = 0
259 - const images = [ 208 + hasMore.value = true
260 - 'https://images.unsplash.com/photo-1567922045116-2a00fae2ed03?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60', 209 +
261 - 'https://images.unsplash.com/photo-1573981368236-719bbb6f70f7?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60', 210 + // 重新加载数据
262 - 'https://images.unsplash.com/photo-1583568671741-c70dafa8e8e7?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60', 211 + await loadVehicleData()
263 - 'https://images.unsplash.com/photo-1595941069915-4ebc5197c14a?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60', 212 + showToast('筛选条件已更新', 'success')
264 - 'https://images.unsplash.com/photo-1558981285-6f0c94958bb6?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60',
265 - 'https://images.unsplash.com/photo-1558981403-c5f9899a28bc?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60'
266 - ]
267 -
268 - const data = []
269 - for (let i = 0; i < size; i++) {
270 - const index = (page - 1) * size + i
271 - const brand = brands[Math.floor(Math.random() * brands.length)]
272 - const school = schools[Math.floor(Math.random() * schools.length)]
273 - const year = years[Math.floor(Math.random() * years.length)]
274 - const image = images[Math.floor(Math.random() * images.length)]
275 -
276 - data.push({
277 - id: `mock_${index + 100}`,
278 - name: `${brand} ${['豪华版', '标准版', '运动版', '经典版'][Math.floor(Math.random() * 4)]}`,
279 - year: year,
280 - school: school,
281 - price: Math.floor(Math.random() * 5000) + 2000,
282 - imageUrl: image,
283 - batteryHealth: Math.floor(Math.random() * 20) + 80,
284 - mileage: Math.floor(Math.random() * 3000) + 500,
285 - brand: brand,
286 - isVerified: Math.random() > 0.7
287 - })
288 - }
289 - return data
290 } 213 }
291 214
292 /** 215 /**
293 - * 模拟异步请求加载更多数据 216 + * 加载车辆数据
217 + * @param {boolean} isLoadMore - 是否为加载更多
294 */ 218 */
295 -const loadMoreData = async () => { 219 +const loadVehicleData = async (isLoadMore = false) => {
296 - if (loading.value || noMoreData.value) return 220 + if (loading.value) return
297 221
298 loading.value = true 222 loading.value = true
299 223
300 try { 224 try {
301 - // 模拟网络请求延迟 225 + // 构建请求参数
302 - await new Promise(resolve => setTimeout(resolve, 1000 + Math.random() * 1000)) 226 + const params = {
303 - 227 + page: currentPage.value,
304 - // 模拟最多加载5页数据 228 + limit: pageSize.value
305 - if (currentPage.value >= 5) {
306 - noMoreData.value = true
307 - loading.value = false
308 - return
309 } 229 }
310 230
311 - currentPage.value++ 231 + // 添加筛选条件
312 - const newData = generateMockData(currentPage.value, pageSize.value) 232 + if (selectedSchool.value) {
313 - scooters.value.push(...newData) 233 + params.school_id = selectedSchool.value
234 + }
235 + if (selectedBrand.value) {
236 + params.brand = selectedBrand.value
237 + }
238 + if (selectedYear.value) {
239 + params.manufacture_year = selectedYear.value
240 + }
241 + if (searchValue.value.trim()) {
242 + params.keyword = searchValue.value.trim()
243 + }
314 244
245 + const response = await getVehicleListAPI(params)
246 +
247 + if (response && response.code === 1 && response.data) {
248 + const vehicleList = response.data.list || []
249 +
250 + // 处理图片数据
251 + const processedData = vehicleList.map(item => ({
252 + ...item,
253 + front_photo: item.front_photo || DEFAULT_COVER_IMG
254 + }))
255 +
256 + if (isLoadMore) {
257 + scooters.value.push(...processedData)
258 + } else {
259 + scooters.value = processedData
260 + }
261 +
262 + // 检查是否还有更多数据 - 基于总数和当前已加载数量
263 + const totalLoaded = (currentPage.value + 1) * pageSize.value
264 + hasMore.value = totalLoaded < response.data.total
265 + } else {
266 + console.error('API返回错误:', response)
267 + showToast(response?.msg || '获取数据失败', 'error')
268 + }
315 } catch (error) { 269 } catch (error) {
316 - Taro.showToast({ 270 + console.error('加载车辆数据失败:', error)
317 - title: '加载失败,请重试', 271 + showToast('网络错误,请稍后重试', 'error')
318 - icon: 'none'
319 - })
320 } finally { 272 } finally {
321 loading.value = false 273 loading.value = false
322 } 274 }
323 } 275 }
276 +
277 +/**
278 + * 点击加载更多数据
279 + */
280 +const loadMoreData = async () => {
281 + if (loading.value || !hasMore.value) return
282 +
283 + currentPage.value++
284 + await loadVehicleData(true)
285 +}
286 +
287 +/**
288 + * 显示提示信息
289 + */
290 +const showToast = (message, type = 'success') => {
291 + toastMessage.value = message
292 + toastType.value = type
293 + toastVisible.value = true
294 +}
295 +
296 +// 初始化
297 +onMounted(async () => {
298 + // 获取全部品牌数据
299 + const vBrands = await getVehicleBrandsAPI()
300 + if (vBrands.code) {
301 + brandOptions.value = vBrands.data.map(brand => ({
302 + text: brand,
303 + value: brand
304 + }))
305 + brandOptions.value = [{
306 + text: '全部品牌',
307 + value: ''
308 + }, ...brandOptions.value]
309 + }
310 + // 生成从当前日期开始往前10年的年份
311 + yearOptions.value = Array.from({ length: 10 }, (_, i) => ({
312 + text: (new Date().getFullYear() - i).toString() + '年',
313 + value: (new Date().getFullYear() - i).toString()
314 + }))
315 + yearOptions.value = [{
316 + text: '全部年份',
317 + value: ''
318 + }, ...yearOptions.value]
319 + // 获取全部学校数据
320 + const schoolData = await getSchoolsAPI()
321 + if (schoolData.code) {
322 + schoolOptions.value = schoolData.data.map(school => ({
323 + text: school.name,
324 + value: school.id
325 + }))
326 + }
327 + schoolOptions.value = [{
328 + text: '全部学校',
329 + value: ''
330 + }, ...schoolOptions.value]
331 +
332 + // 加载初始车辆数据
333 + await loadVehicleData()
334 +})
324 </script> 335 </script>
325 336
326 <style lang="less"> 337 <style lang="less">
......