hookehuyr

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

- 替换模拟数据为真实接口数据
- 添加品牌、年份和学校筛选功能
- 实现分页加载更多功能
- 添加加载状态和提示信息
......@@ -34,9 +34,9 @@
class="bg-white rounded-lg shadow-sm overflow-hidden mb-3" @tap="() => onProductClick(scooter)">
<view class="flex">
<view class="w-32 h-24 relative p-2">
<image :src="scooter.imageUrl" :alt="scooter.name" mode="aspectFill"
<image :src="scooter.front_photo" mode="aspectFill"
class="w-full h-full object-cover rounded-lg" />
<view v-if="scooter.isVerified"
<view v-if="scooter.verification_status === 5"
class="absolute bottom-3 right-3 bg-orange-500 text-white text-xs px-1 rounded flex items-center">
<Check size="12" color="#ffffff" class="mr-0.5" />
<text class="text-white">认证</text>
......@@ -47,20 +47,20 @@
<Heart1 v-if="!scooter.is_favorite" size="22" color="#9ca3af" />
<HeartFill v-else size="22" color="#ef4444" />
</view>
<text class="font-medium text-sm block">{{ scooter.name }}</text>
<text class="font-medium text-sm block">{{ scooter.brand }} {{ scooter.model }}</text>
<text class="text-xs text-gray-600 mt-1 block">
{{ scooter.year }} ·
<text v-if="scooter.batteryHealth">电池健康度{{ scooter.batteryHealth }}%</text>
<text v-if="scooter.mileage"> 行驶{{ scooter.mileage }}公里</text>
{{ scooter.manufacture_year }} ·
<text v-if="scooter.range_km">续航{{ scooter.range_km }}km</text>
<text v-if="scooter.max_speed_kmh"> 最高时速{{ scooter.max_speed_kmh }}km/h</text>
</text>
<view class="mt-2">
<text class="text-orange-500 font-bold" style="font-size: 1.2rem;">
¥{{ scooter.price.toLocaleString() }}
</text>
<text v-if="scooter.isVerified" class="ml-2 text-xs px-1 py-0.5 bg-orange-100 text-orange-500 rounded">
<!-- <text v-if="scooter.isVerified" class="ml-2 text-xs px-1 py-0.5 bg-orange-100 text-orange-500 rounded">
低于市场价 10%
</text>
<text class="text-xs text-gray-500 mt-1 block">{{ scooter.school }}</text>
</text> -->
<text class="text-xs text-gray-500 mt-1 block">{{ scooter.school_name }}</text>
</view>
</view>
</view>
......@@ -74,11 +74,11 @@
<text class="loading-text">加载中...</text>
</view>
<view v-else-if="noMoreData" class="no-more-data">
<view v-else-if="!hasMore && scooters.length > 0" class="no-more-data">
<text>没有更多数据了</text>
</view>
<nut-button v-else shape="round" type="default" plain @click="loadMoreData">点击加载更多</nut-button>
<nut-button v-else-if="hasMore" shape="round" type="default" plain @click="loadMoreData">点击加载更多</nut-button>
</view>
</view>
......@@ -90,112 +90,64 @@
<!-- 自定义TabBar -->
<TabBar />
<!-- 成功提示 -->
<nut-toast
v-model:visible="toastVisible"
:msg="toastMessage"
:type="toastType"
/>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { ref, onMounted } from 'vue'
import Taro from '@tarojs/taro'
import { Search2, Check, Heart1, HeartFill } from '@nutui/icons-vue-taro'
import TabBar from '@/components/TabBar.vue'
import FeaturedRecommendations from '@/components/FeaturedRecommendations.vue'
import { useFavorite } from '@/composables/useFavorite'
// 接口导入
import { getVehicleBrandsAPI, getSchoolsAPI } from '@/api/other';
import { getVehicleListAPI } from '@/api/car';
import { DEFAULT_COVER_IMG } from '@/utils/config'
// 响应式数据
const searchValue = ref('')
const onBlurSearch = () => {
console.warn(searchValue.value)
/**
* 搜索框失焦事件
*/
const onBlurSearch = async () => {
// 重置分页并重新加载数据
currentPage.value = 0
hasMore.value = true
await loadVehicleData()
}
// 收藏功能现在使用基于对象属性的模式
// 无限滚动相关状态
// 分页相关状态
const loading = ref(false)
const noMoreData = ref(false)
const currentPage = ref(1)
const pageSize = ref(4)
const hasMore = ref(true)
const currentPage = ref(0)
const pageSize = ref(10)
// Toast提示
const toastVisible = ref(false)
const toastMessage = ref('')
const toastType = ref('success')
// Filter states - 使用NutUI Menu组件
const selectedBrand = ref('全部品牌')
const selectedYear = ref('出厂年份')
const selectedSchool = ref('所在学校')
const selectedBrand = ref('')
const selectedYear = ref('')
const selectedSchool = ref('')
// Menu选项数据
const brandOptions = ref([
{ text: '全部品牌', value: '全部品牌' },
{ text: '雅迪', value: '雅迪' },
{ text: '台铃', value: '台铃' },
{ text: '小鸟', value: '小鸟' },
{ text: '新日', value: '新日' },
{ text: '爱玛', value: '爱玛' },
{ text: '小牛', value: '小牛' }
])
const yearOptions = ref([
{ text: '出厂年份', value: '出厂年份' },
{ text: '2024年', value: '2024年' },
{ text: '2023年', value: '2023年' },
{ text: '2022年', value: '2022年' },
{ text: '2021年', value: '2021年' },
{ text: '2020年', value: '2020年' }
])
const schoolOptions = ref([
{ text: '所在学校', value: '所在学校' },
{ text: '上海理工大学', value: '上海理工大学' },
{ text: '上海复旦大学', value: '上海复旦大学' },
{ text: '上海同济大学', value: '上海同济大学' },
{ text: '上海交通大学', value: '上海交通大学' }
])
// 主要车辆列表数据 - 参考PostPage.tsx
const scooters = ref([
{
id: '5',
name: '雅迪 豪华版',
year: '2023年',
school: '上海理工大学',
price: 3200,
imageUrl: 'https://images.unsplash.com/photo-1567922045116-2a00fae2ed03?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60',
batteryHealth: 98,
mileage: 1200,
brand: '雅迪',
isVerified: true
},
{
id: '6',
name: '台铃 战速',
year: '2022年',
school: '上海理工大学',
price: 3800,
imageUrl: 'https://images.unsplash.com/photo-1567922045116-2a00fae2ed03?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60',
batteryHealth: 92,
mileage: 2500,
brand: '台铃'
},
{
id: '7',
name: '小鸟电车',
year: '2023年',
school: '上海复旦大学',
price: 3100,
imageUrl: 'https://images.unsplash.com/photo-1567922045116-2a00fae2ed03?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60',
batteryHealth: 92,
mileage: 2000,
brand: '小鸟'
},
{
id: '8',
name: '新日电动车',
year: '2024年',
school: '上海同济大学',
price: 6700,
imageUrl: 'https://images.unsplash.com/photo-1595941069915-4ebc5197c14a?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60',
batteryHealth: 96,
mileage: 500,
brand: '新日',
isVerified: true
}
])
const brandOptions = ref([])
const yearOptions = ref([])
const schoolOptions = ref([])
// 车辆列表数据
const scooters = ref([])
......@@ -225,7 +177,7 @@ const onProductClick = (scooter) => {
*/
const onBrandChange = (value) => {
selectedBrand.value = value
// 这里可以添加过滤逻辑
filterCars()
}
/**
......@@ -234,7 +186,7 @@ const onBrandChange = (value) => {
*/
const onYearChange = (value) => {
selectedYear.value = value
// 这里可以添加过滤逻辑
filterCars()
}
/**
......@@ -243,84 +195,143 @@ const onYearChange = (value) => {
*/
const onSchoolChange = (value) => {
selectedSchool.value = value
// 这里可以添加过滤逻辑
filterCars()
}
/**
* 生成模拟车辆数据
* @param {number} page - 页码
* @param {number} size - 每页数量
* @returns {Array} 车辆数据数组
* 过滤车辆数据
*/
const generateMockData = (page, size) => {
const brands = ['雅迪', '台铃', '小鸟', '新日', '爱玛', '小牛', '绿源', '立马']
const schools = ['上海理工大学', '上海复旦大学', '上海同济大学', '上海交通大学', '华东师范大学', '上海大学']
const years = ['2024年', '2023年', '2022年', '2021年', '2020年']
const images = [
'https://images.unsplash.com/photo-1567922045116-2a00fae2ed03?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60',
'https://images.unsplash.com/photo-1573981368236-719bbb6f70f7?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60',
'https://images.unsplash.com/photo-1583568671741-c70dafa8e8e7?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60',
'https://images.unsplash.com/photo-1595941069915-4ebc5197c14a?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60',
'https://images.unsplash.com/photo-1558981285-6f0c94958bb6?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60',
'https://images.unsplash.com/photo-1558981403-c5f9899a28bc?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60'
]
const data = []
for (let i = 0; i < size; i++) {
const index = (page - 1) * size + i
const brand = brands[Math.floor(Math.random() * brands.length)]
const school = schools[Math.floor(Math.random() * schools.length)]
const year = years[Math.floor(Math.random() * years.length)]
const image = images[Math.floor(Math.random() * images.length)]
data.push({
id: `mock_${index + 100}`,
name: `${brand} ${['豪华版', '标准版', '运动版', '经典版'][Math.floor(Math.random() * 4)]}`,
year: year,
school: school,
price: Math.floor(Math.random() * 5000) + 2000,
imageUrl: image,
batteryHealth: Math.floor(Math.random() * 20) + 80,
mileage: Math.floor(Math.random() * 3000) + 500,
brand: brand,
isVerified: Math.random() > 0.7
})
}
return data
const filterCars = async () => {
// 重置数据
scooters.value = []
currentPage.value = 0
hasMore.value = true
// 重新加载数据
await loadVehicleData()
showToast('筛选条件已更新', 'success')
}
/**
* 模拟异步请求加载更多数据
* 加载车辆数据
* @param {boolean} isLoadMore - 是否为加载更多
*/
const loadMoreData = async () => {
if (loading.value || noMoreData.value) return
const loadVehicleData = async (isLoadMore = false) => {
if (loading.value) return
loading.value = true
try {
// 模拟网络请求延迟
await new Promise(resolve => setTimeout(resolve, 1000 + Math.random() * 1000))
// 模拟最多加载5页数据
if (currentPage.value >= 5) {
noMoreData.value = true
loading.value = false
return
// 构建请求参数
const params = {
page: currentPage.value,
limit: pageSize.value
}
currentPage.value++
const newData = generateMockData(currentPage.value, pageSize.value)
scooters.value.push(...newData)
// 添加筛选条件
if (selectedSchool.value) {
params.school_id = selectedSchool.value
}
if (selectedBrand.value) {
params.brand = selectedBrand.value
}
if (selectedYear.value) {
params.manufacture_year = selectedYear.value
}
if (searchValue.value.trim()) {
params.keyword = searchValue.value.trim()
}
const response = await getVehicleListAPI(params)
if (response && response.code === 1 && response.data) {
const vehicleList = response.data.list || []
// 处理图片数据
const processedData = vehicleList.map(item => ({
...item,
front_photo: item.front_photo || DEFAULT_COVER_IMG
}))
if (isLoadMore) {
scooters.value.push(...processedData)
} else {
scooters.value = processedData
}
// 检查是否还有更多数据 - 基于总数和当前已加载数量
const totalLoaded = (currentPage.value + 1) * pageSize.value
hasMore.value = totalLoaded < response.data.total
} else {
console.error('API返回错误:', response)
showToast(response?.msg || '获取数据失败', 'error')
}
} catch (error) {
Taro.showToast({
title: '加载失败,请重试',
icon: 'none'
})
console.error('加载车辆数据失败:', error)
showToast('网络错误,请稍后重试', 'error')
} finally {
loading.value = false
}
}
/**
* 点击加载更多数据
*/
const loadMoreData = async () => {
if (loading.value || !hasMore.value) return
currentPage.value++
await loadVehicleData(true)
}
/**
* 显示提示信息
*/
const showToast = (message, type = 'success') => {
toastMessage.value = message
toastType.value = type
toastVisible.value = true
}
// 初始化
onMounted(async () => {
// 获取全部品牌数据
const vBrands = await getVehicleBrandsAPI()
if (vBrands.code) {
brandOptions.value = vBrands.data.map(brand => ({
text: brand,
value: brand
}))
brandOptions.value = [{
text: '全部品牌',
value: ''
}, ...brandOptions.value]
}
// 生成从当前日期开始往前10年的年份
yearOptions.value = Array.from({ length: 10 }, (_, i) => ({
text: (new Date().getFullYear() - i).toString() + '年',
value: (new Date().getFullYear() - i).toString()
}))
yearOptions.value = [{
text: '全部年份',
value: ''
}, ...yearOptions.value]
// 获取全部学校数据
const schoolData = await getSchoolsAPI()
if (schoolData.code) {
schoolOptions.value = schoolData.data.map(school => ({
text: school.name,
value: school.id
}))
}
schoolOptions.value = [{
text: '全部学校',
value: ''
}, ...schoolOptions.value]
// 加载初始车辆数据
await loadVehicleData()
})
</script>
<style lang="less">
......