hookehuyr

feat(SearchPopup): 替换模拟数据为真实API接口调用

- 移除模拟数据生成逻辑,改为调用真实API获取车辆数据
- 添加品牌、年份和学校筛选选项的动态加载
- 修改车辆展示字段以匹配API返回数据结构
- 优化搜索逻辑,支持多种筛选条件组合
- 添加分页加载更多功能
1 <!-- 1 <!--
2 * @Date: 2025-01-20 00:00:00 2 * @Date: 2025-01-20 00:00:00
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-07-06 18:08:56 4 + * @LastEditTime: 2025-07-11 11:29:51
5 * @FilePath: /jgdl/src/components/SearchPopup.vue 5 * @FilePath: /jgdl/src/components/SearchPopup.vue
6 * @Description: 搜索弹窗组件 6 * @Description: 搜索弹窗组件
7 --> 7 -->
...@@ -63,24 +63,24 @@ ...@@ -63,24 +63,24 @@
63 <view v-for="scooter in searchResults" :key="scooter.id" 63 <view v-for="scooter in searchResults" :key="scooter.id"
64 class="bg-white rounded-lg shadow-sm overflow-hidden" @tap="() => onProductClick(scooter)"> 64 class="bg-white rounded-lg shadow-sm overflow-hidden" @tap="() => onProductClick(scooter)">
65 <view class="relative p-2"> 65 <view class="relative p-2">
66 - <image :src="scooter.imageUrl" :alt="scooter.name" mode="aspectFill" 66 + <image :src="scooter.front_photo" mode="aspectFill"
67 class="w-full h-36 object-cover rounded-lg" /> 67 class="w-full h-36 object-cover rounded-lg" />
68 <view class="absolute top-4 right-3 w-7 h-7 rounded-full bg-white bg-opacity-80 flex items-center justify-center" @tap.stop="() => toggleFavorite(scooter)"> 68 <view class="absolute top-4 right-3 w-7 h-7 rounded-full bg-white bg-opacity-80 flex items-center justify-center" @tap.stop="() => toggleFavorite(scooter)">
69 <Heart1 v-if="!scooter.is_favorite" size="22" :color="'#9ca3af'" /> 69 <Heart1 v-if="!scooter.is_favorite" size="22" :color="'#9ca3af'" />
70 <HeartFill v-else size="22" :color="'#ef4444'" /> 70 <HeartFill v-else size="22" :color="'#ef4444'" />
71 </view> 71 </view>
72 - <view v-if="scooter.isVerified" 72 + <view v-if="scooter.verification_status === 5"
73 class="absolute bottom-4 right-4 text-white text-xs px-1.5 py-0.5 rounded flex items-center" style="background-color: #EB5305;"> 73 class="absolute bottom-4 right-4 text-white text-xs px-1.5 py-0.5 rounded flex items-center" style="background-color: #EB5305;">
74 <Check size="12" color="#ffffff" class="mr-0.5" /> 74 <Check size="12" color="#ffffff" class="mr-0.5" />
75 <text class="text-white">认证</text> 75 <text class="text-white">认证</text>
76 </view> 76 </view>
77 </view> 77 </view>
78 <view class="p-2 pl-3"> 78 <view class="p-2 pl-3">
79 - <text class="font-medium text-sm block">{{ scooter.name }}</text> 79 + <text class="font-medium text-sm block">{{ scooter.brand }} {{ scooter.model }}</text>
80 <text class="text-xs text-gray-500 block mt-1 mb-1"> 80 <text class="text-xs text-gray-500 block mt-1 mb-1">
81 - {{ scooter.year }} · {{ scooter.school }} 81 + {{ scooter.manufacture_year }} · {{ scooter.school_name }}
82 - <text v-if="scooter.batteryHealth">电池健康度{{ scooter.batteryHealth }}%</text> 82 + <text v-if="scooter.range_km">续航{{ scooter.range_km }}km</text>
83 - <text v-if="scooter.mileage"> 行驶{{ scooter.mileage }}公里</text> 83 + <text v-if="scooter.max_speed_kmh"> 最高时速{{ scooter.max_speed_kmh }}km/h</text>
84 </text> 84 </text>
85 <view class="mt-1"> 85 <view class="mt-1">
86 <text class="text-orange-500 font-bold" style="font-size: 1.25rem;"> 86 <text class="text-orange-500 font-bold" style="font-size: 1.25rem;">
...@@ -128,11 +128,15 @@ ...@@ -128,11 +128,15 @@
128 </template> 128 </template>
129 129
130 <script setup> 130 <script setup>
131 -import { ref, computed, watch } from 'vue' 131 +import { ref, computed, watch, onMounted } from 'vue'
132 import Taro from '@tarojs/taro' 132 import Taro from '@tarojs/taro'
133 import { Search2, Check, Heart1, HeartFill } from '@nutui/icons-vue-taro' 133 import { Search2, Check, Heart1, HeartFill } from '@nutui/icons-vue-taro'
134 import { useFavorite } from '@/composables/useFavorite' 134 import { useFavorite } from '@/composables/useFavorite'
135 import "./SearchPopup.less" 135 import "./SearchPopup.less"
136 +// 接口导入
137 +import { getVehicleBrandsAPI, getSchoolsAPI } from '@/api/other';
138 +import { getVehicleListAPI } from '@/api/car';
139 +import { DEFAULT_COVER_IMG } from '@/utils/config'
136 140
137 // Props 141 // Props
138 const props = defineProps({ 142 const props = defineProps({
...@@ -159,42 +163,21 @@ const hasSearched = ref(false) ...@@ -159,42 +163,21 @@ const hasSearched = ref(false)
159 // 滚动相关 163 // 滚动相关
160 const loading = ref(false) 164 const loading = ref(false)
161 const noMoreData = ref(false) 165 const noMoreData = ref(false)
162 -const currentPage = ref(1) 166 +const currentPage = ref(0)
163 const pageSize = ref(10) 167 const pageSize = ref(10)
164 const totalCount = ref(0) 168 const totalCount = ref(0)
165 169
166 // Filter states 170 // Filter states
167 -const selectedBrand = ref('全部品牌') 171 +const selectedBrand = ref('')
168 -const selectedYear = ref('出厂年份') 172 +const selectedYear = ref('')
169 -const selectedSchool = ref('所在学校') 173 +const selectedSchool = ref('')
170 174
171 // Menu选项数据 175 // Menu选项数据
172 -const brandOptions = ref([ 176 +const brandOptions = ref([])
173 - { text: '全部品牌', value: '全部品牌' }, 177 +
174 - { text: '雅迪', value: '雅迪' }, 178 +const yearOptions = ref([])
175 - { text: '台铃', value: '台铃' }, 179 +
176 - { text: '小鸟', value: '小鸟' }, 180 +const schoolOptions = ref([])
177 - { text: '新日', value: '新日' },
178 - { text: '爱玛', value: '爱玛' },
179 - { text: '小牛', value: '小牛' }
180 -])
181 -
182 -const yearOptions = ref([
183 - { text: '出厂年份', value: '出厂年份' },
184 - { text: '2024年', value: '2024年' },
185 - { text: '2023年', value: '2023年' },
186 - { text: '2022年', value: '2022年' },
187 - { text: '2021年', value: '2021年' },
188 - { text: '2020年', value: '2020年' }
189 -])
190 -
191 -const schoolOptions = ref([
192 - { text: '所在学校', value: '所在学校' },
193 - { text: '上海理工大学', value: '上海理工大学' },
194 - { text: '上海复旦大学', value: '上海复旦大学' },
195 - { text: '上海同济大学', value: '上海同济大学' },
196 - { text: '上海交通大学', value: '上海交通大学' }
197 -])
198 181
199 // 搜索结果数据 182 // 搜索结果数据
200 const searchResults = ref([]) 183 const searchResults = ref([])
...@@ -207,70 +190,78 @@ const scrollStyle = computed(() => { ...@@ -207,70 +190,78 @@ const scrollStyle = computed(() => {
207 }) 190 })
208 191
209 /** 192 /**
210 - * 生成模拟数据 193 + * 加载车辆数据
211 - * @param {number} page 页码 194 + * @param {boolean} isLoadMore - 是否为加载更多
212 - * @param {number} size 每页数量
213 - * @returns {Array} 模拟数据数组
214 */ 195 */
215 -const generateMockData = (page, size) => { 196 +const loadVehicleData = async (isLoadMore = false) => {
216 - const brands = ['雅迪', '台铃', '小鸟', '新日', '爱玛', '小牛'] 197 + if (loading.value) return
217 - const schools = ['上海理工大学', '上海复旦大学', '上海同济大学', '上海交通大学'] 198 +
218 - const years = ['2024年', '2023年', '2022年', '2021年', '2020年'] 199 + loading.value = true
219 - const images = [ 200 +
220 - 'https://images.unsplash.com/photo-1558981285-6f0c94958bb6?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60', 201 + try {
221 - 'https://images.unsplash.com/photo-1558981403-c5f9899a28bc?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60', 202 + // 构建请求参数
222 - 'https://images.unsplash.com/photo-1591637333184-19aa84b3e01f?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60', 203 + const params = {
223 - 'https://images.unsplash.com/photo-1558980664-3a031cf67ea8?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60', 204 + page: currentPage.value,
224 - 'https://images.unsplash.com/photo-1595941069915-4ebc5197c14a?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60' 205 + limit: pageSize.value
225 - ] 206 + }
226 - 207 +
227 - const data = [] 208 + // 添加筛选条件
228 - const startId = (page - 1) * size + 1 209 + if (selectedSchool.value) {
229 - 210 + params.school_id = selectedSchool.value
230 - for (let i = 0; i < size; i++) { 211 + }
231 - const brand = brands[Math.floor(Math.random() * brands.length)] 212 + if (selectedBrand.value) {
232 - const school = schools[Math.floor(Math.random() * schools.length)] 213 + params.brand = selectedBrand.value
233 - const year = years[Math.floor(Math.random() * years.length)] 214 + }
234 - const image = images[Math.floor(Math.random() * images.length)] 215 + if (selectedYear.value) {
235 - 216 + params.manufacture_year = selectedYear.value
236 - data.push({ 217 + }
237 - id: `search_${startId + i}`, 218 + if (searchValue.value.trim()) {
238 - name: `${brand} ${['豪华版', '标准版', '运动版', '经典版'][Math.floor(Math.random() * 4)]}`, 219 + params.keyword = searchValue.value.trim()
239 - year: year, 220 + }
240 - school: school, 221 +
241 - price: Math.floor(Math.random() * 5000) + 2000, 222 + const response = await getVehicleListAPI(params)
242 - imageUrl: image,
243 - batteryHealth: Math.floor(Math.random() * 30) + 70,
244 - mileage: Math.floor(Math.random() * 5000) + 500,
245 - brand: brand,
246 - isVerified: Math.random() > 0.6
247 - })
248 - }
249 223
250 - return data 224 + if (response && response.code === 1 && response.data) {
225 + const vehicleList = response.data.list || []
226 +
227 + // 处理图片数据
228 + const processedData = vehicleList.map(item => ({
229 + ...item,
230 + front_photo: item.front_photo || DEFAULT_COVER_IMG
231 + }))
232 +
233 + if (isLoadMore) {
234 + searchResults.value.push(...processedData)
235 + } else {
236 + searchResults.value = processedData
237 + }
238 +
239 + // 更新总数
240 + totalCount.value = response.data.total || 0
241 +
242 + // 检查是否还有更多数据
243 + const totalLoaded = (currentPage.value + 1) * pageSize.value
244 + noMoreData.value = totalLoaded >= totalCount.value
245 + } else {
246 + console.error('API返回错误:', response)
247 + }
248 + } catch (error) {
249 + console.error('加载车辆数据失败:', error)
250 + } finally {
251 + loading.value = false
252 + }
251 } 253 }
252 254
253 /** 255 /**
254 * 执行搜索 256 * 执行搜索
255 */ 257 */
256 -const performSearch = () => { 258 +const performSearch = async () => {
257 - if (!searchValue.value.trim()) { 259 + // 重置分页
258 - clearSearchResults() 260 + currentPage.value = 0
259 - return
260 - }
261 -
262 - loading.value = true
263 - hasSearched.value = true
264 - currentPage.value = 1
265 noMoreData.value = false 261 noMoreData.value = false
262 + hasSearched.value = true
266 263
267 - // 模拟API调用延迟 264 + await loadVehicleData()
268 - setTimeout(() => {
269 - const mockData = generateMockData(1, pageSize.value)
270 - searchResults.value = mockData
271 - totalCount.value = 50 // 模拟总数
272 - loading.value = false
273 - }, 500)
274 } 265 }
275 266
276 /** 267 /**
...@@ -279,16 +270,25 @@ const performSearch = () => { ...@@ -279,16 +270,25 @@ const performSearch = () => {
279 const clearSearchResults = () => { 270 const clearSearchResults = () => {
280 searchResults.value = [] 271 searchResults.value = []
281 hasSearched.value = false 272 hasSearched.value = false
282 - currentPage.value = 1 273 + currentPage.value = 0
283 noMoreData.value = false 274 noMoreData.value = false
284 totalCount.value = 0 275 totalCount.value = 0
285 } 276 }
286 277
287 /** 278 /**
279 + * 检查是否有搜索条件
280 + */
281 +const hasSearchConditions = () => {
282 + return searchValue.value.trim() || selectedBrand.value || selectedYear.value || selectedSchool.value
283 +}
284 +
285 +/**
288 * 搜索事件处理 286 * 搜索事件处理
289 */ 287 */
290 const onSearch = () => { 288 const onSearch = () => {
291 - performSearch() 289 + if (hasSearchConditions()) {
290 + performSearch()
291 + }
292 } 292 }
293 293
294 /** 294 /**
...@@ -297,21 +297,24 @@ const onSearch = () => { ...@@ -297,21 +297,24 @@ const onSearch = () => {
297 const onBlurSearch = () => { 297 const onBlurSearch = () => {
298 if (searchValue.value.trim()) { 298 if (searchValue.value.trim()) {
299 performSearch() 299 performSearch()
300 - } else { 300 + } else if (!hasSearchConditions()) {
301 - // 如果搜索框为空,清除搜索结果 301 + // 如果搜索框为空且没有其他筛选条件,清除搜索结果
302 clearSearchResults() 302 clearSearchResults()
303 } 303 }
304 } 304 }
305 305
306 const onClearSearch = () => { 306 const onClearSearch = () => {
307 - clearSearchResults() 307 + if (!hasSearchConditions()) {
308 + clearSearchResults()
309 + }
308 } 310 }
309 311
310 /** 312 /**
311 * 品牌筛选变化 313 * 品牌筛选变化
312 */ 314 */
313 const onBrandChange = () => { 315 const onBrandChange = () => {
314 - if (hasSearched.value) { 316 + // 如果有搜索条件或者之前有搜索历史,则执行搜索
317 + if (hasSearchConditions() || hasSearched.value) {
315 performSearch() 318 performSearch()
316 } 319 }
317 } 320 }
...@@ -320,7 +323,8 @@ const onBrandChange = () => { ...@@ -320,7 +323,8 @@ const onBrandChange = () => {
320 * 年份筛选变化 323 * 年份筛选变化
321 */ 324 */
322 const onYearChange = () => { 325 const onYearChange = () => {
323 - if (hasSearched.value) { 326 + // 如果有搜索条件或者之前有搜索历史,则执行搜索
327 + if (hasSearchConditions() || hasSearched.value) {
324 performSearch() 328 performSearch()
325 } 329 }
326 } 330 }
...@@ -329,7 +333,8 @@ const onYearChange = () => { ...@@ -329,7 +333,8 @@ const onYearChange = () => {
329 * 学校筛选变化 333 * 学校筛选变化
330 */ 334 */
331 const onSchoolChange = () => { 335 const onSchoolChange = () => {
332 - if (hasSearched.value) { 336 + // 如果有搜索条件或者之前有搜索历史,则执行搜索
337 + if (hasSearchConditions() || hasSearched.value) {
333 performSearch() 338 performSearch()
334 } 339 }
335 } 340 }
...@@ -337,24 +342,11 @@ const onSchoolChange = () => { ...@@ -337,24 +342,11 @@ const onSchoolChange = () => {
337 /** 342 /**
338 * 加载更多数据 343 * 加载更多数据
339 */ 344 */
340 -const loadMore = () => { 345 +const loadMore = async () => {
341 - if (loading.value || noMoreData.value) return 346 + if (loading.value || noMoreData.value || !hasSearched.value) return
342 347
343 - loading.value = true
344 currentPage.value++ 348 currentPage.value++
345 - 349 + await loadVehicleData(true)
346 - // 模拟API调用延迟
347 - setTimeout(() => {
348 - const mockData = generateMockData(currentPage.value, pageSize.value)
349 -
350 - if (mockData.length === 0 || searchResults.value.length >= totalCount.value) {
351 - noMoreData.value = true
352 - } else {
353 - searchResults.value.push(...mockData)
354 - }
355 -
356 - loading.value = false
357 - }, 500)
358 } 350 }
359 351
360 // 使用收藏功能composables 352 // 使用收藏功能composables
...@@ -390,14 +382,62 @@ const resetSearchState = () => { ...@@ -390,14 +382,62 @@ const resetSearchState = () => {
390 hasSearched.value = false 382 hasSearched.value = false
391 loading.value = false 383 loading.value = false
392 noMoreData.value = false 384 noMoreData.value = false
393 - currentPage.value = 1 385 + currentPage.value = 0
394 totalCount.value = 0 386 totalCount.value = 0
395 // 重置筛选条件 387 // 重置筛选条件
396 - selectedBrand.value = '全部品牌' 388 + selectedBrand.value = ''
397 - selectedYear.value = '出厂年份' 389 + selectedYear.value = ''
398 - selectedSchool.value = '所在学校' 390 + selectedSchool.value = ''
391 +}
392 +
393 +// 初始化筛选选项数据
394 +const initFilterOptions = async () => {
395 + try {
396 + // 获取全部品牌数据
397 + const vBrands = await getVehicleBrandsAPI()
398 + if (vBrands.code) {
399 + brandOptions.value = vBrands.data.map(brand => ({
400 + text: brand,
401 + value: brand
402 + }))
403 + brandOptions.value = [{
404 + text: '全部品牌',
405 + value: ''
406 + }, ...brandOptions.value]
407 + }
408 +
409 + // 生成从当前日期开始往前10年的年份
410 + yearOptions.value = Array.from({ length: 10 }, (_, i) => ({
411 + text: (new Date().getFullYear() - i).toString() + '年',
412 + value: (new Date().getFullYear() - i).toString()
413 + }))
414 + yearOptions.value = [{
415 + text: '全部年份',
416 + value: ''
417 + }, ...yearOptions.value]
418 +
419 + // 获取全部学校数据
420 + const schoolData = await getSchoolsAPI()
421 + if (schoolData.code) {
422 + schoolOptions.value = schoolData.data.map(school => ({
423 + text: school.name,
424 + value: school.id
425 + }))
426 + }
427 + schoolOptions.value = [{
428 + text: '全部学校',
429 + value: ''
430 + }, ...schoolOptions.value]
431 + } catch (error) {
432 + console.error('初始化筛选选项失败:', error)
433 + }
399 } 434 }
400 435
436 +// 组件挂载时初始化筛选选项
437 +onMounted(() => {
438 + initFilterOptions()
439 +})
440 +
401 // 监听visible.value的变化 441 // 监听visible.value的变化
402 watch(visible, (newVisible) => { 442 watch(visible, (newVisible) => {
403 if (newVisible) { 443 if (newVisible) {
......