hookehuyr

feat(首页): 新增最新上架组件并优化首页结构

将最新上架列表抽离为独立组件 LatestScooters
优化数据加载逻辑,支持分页查询和错误处理
移除首页冗余代码,保持组件单一职责
......@@ -9,6 +9,7 @@ declare module 'vue' {
export interface GlobalComponents {
BrandModelPicker: typeof import('./src/components/BrandModelPicker.vue')['default']
FeaturedRecommendations: typeof import('./src/components/FeaturedRecommendations.vue')['default']
LatestScooters: typeof import('./src/components/LatestScooters.vue')['default']
NavBar: typeof import('./src/components/navBar.vue')['default']
NutActionSheet: typeof import('@nutui/nutui-taro')['ActionSheet']
NutButton: typeof import('@nutui/nutui-taro')['Button']
......
<template>
<view class="latest-scooters">
<!-- 最新上架 -->
<view class="px-4 py-3">
<view class="flex items-center justify-between mb-3">
<text class="text-lg font-medium">最新上架</text>
<view class="text-sm text-gray-500 flex items-center" @tap="onNewCarClick">
<text>更多</text>
<RectRight size="12" />
</view>
</view>
<view class="space-y-4">
<view v-for="scooter in latestScooters" :key="scooter.id"
class="bg-white rounded-lg shadow-sm overflow-hidden" @tap="() => onProductClick(scooter)">
<view class="flex min-h-32">
<view class="w-32 relative p-2 flex flex-col">
<image :src="scooter.front_photo" mode="aspectFill"
class="w-full flex-1 object-cover rounded-lg" />
<view v-if="scooter.verification_status === 5"
class="absolute bottom-3 right-3 text-white text-xs px-1 rounded flex items-center"
style="background-color: #EB5305;">
<Check size="12" color="#ffffff" class="mr-0.5" />
<text class="text-white">认证</text>
</view>
</view>
<view class="flex-1 p-3 relative flex flex-col">
<view class="absolute top-2 right-2" @tap.stop="() => toggleFavorite(scooter)">
<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.brand }} {{ scooter.model }}</text>
<text class="text-xs text-gray-600 mt-1 block" style="word-break: break-all;">
{{ 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-auto">
<text class="text-orange-500 font-bold" style="font-size: 1.25rem;">
¥{{ scooter.price.toLocaleString() }}
</text>
<text class="text-xs text-gray-500 mt-1 block">{{ scooter.school_name }}</text>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import Taro from '@tarojs/taro'
import { ref, onMounted } from 'vue'
import { RectRight, Check, Heart1, HeartFill } from '@nutui/icons-vue-taro'
import { getVehicleListAPI } from '@/api/car'
import { useFavorite } from '@/composables/useFavorite'
import { DEFAULT_COVER_IMG } from '@/utils/config'
// 最新上架数据
const latestScooters = ref([])
// 使用收藏功能composables
const { toggleFavorite } = useFavorite()
/**
* 点击产品卡片
* @param {Object} scooter - 电动车信息
*/
const onProductClick = (scooter) => {
Taro.navigateTo({
url: `/pages/productDetail/index?id=${scooter.id}`
})
}
/**
* 点击查看更多
*/
const onNewCarClick = () => {
Taro.navigateTo({
url: '/pages/newCarList/index'
})
}
/**
* 加载最新上架数据
* 循环查询直到获取到数据或确认没有数据为止
*/
const loadLatestData = async () => {
try {
let page = 0
let hasData = false
let allData = []
const limit = 5
// 循环查询直到获取到足够数据或没有更多数据
while (!hasData && page < 10) { // 最多查询10页防止无限循环
const res = await getVehicleListAPI({ page, limit })
if (res.code && res.data && res.data.list && res.data.list.length > 0) {
// 处理图片数据
const processedData = res.data.list.map(item => ({
...item,
front_photo: item.front_photo || DEFAULT_COVER_IMG,
// 确保价格为数字类型
price: Number(item.price) || 0,
market_price: Number(item.market_price) || 0,
}))
allData = [...allData, ...processedData]
// 如果已经获取到足够数据,停止查询
if (allData.length >= limit) {
hasData = true
latestScooters.value = allData.slice(0, limit)
} else if (res.data.list.length < limit) {
// 如果返回的数据少于请求的数量,说明没有更多数据了
hasData = true
latestScooters.value = allData
}
} else {
// 如果当前页没有数据,检查是否还有下一页
if (res.data && res.data.total !== undefined) {
const totalPages = Math.ceil(res.data.total / limit)
if (page >= totalPages - 1) {
// 已经是最后一页,停止查询
break
}
} else {
// 没有总数信息,如果连续没有数据就停止
break
}
}
page++
}
// 如果最终没有获取到任何数据
if (allData.length === 0) {
latestScooters.value = []
}
} catch (error) {
console.error('加载最新上架数据失败:', error)
latestScooters.value = []
}
}
onMounted(() => {
loadLatestData()
})
// 暴露刷新方法供父组件调用
defineExpose({
refresh: loadLatestData
})
</script>
<style lang="less" scoped>
.latest-scooters {
// 组件样式可以根据需要添加
}
</style>
<!--
* @Date: 2025-06-28 10:33:00
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-07-16 09:25:38
* @LastEditTime: 2025-07-16 09:37:40
* @FilePath: /jgdl/src/pages/index/index.vue
* @Description: 捡个电驴首页
-->
......@@ -68,50 +68,7 @@
<FeaturedRecommendations />
<!-- Latest Listings -->
<view class="px-4 mt-6 mb-20">
<view class="flex justify-between items-center mb-2">
<text class="text-lg font-medium">最新上架</text>
<view class="text-sm text-gray-500 flex items-center" @tap="onNewCarClick">
<text>更多</text>
<RectRight size="12" />
</view>
</view>
<view class="flex flex-col">
<view v-for="scooter in latestScooters" :key="scooter.id"
class="bg-white rounded-lg shadow-sm overflow-hidden mb-3" @tap="() => onProductClick(scooter)">
<view class="flex min-h-32">
<view class="w-32 relative p-2 flex flex-col">
<image :src="scooter.front_photo" :alt="scooter.name" mode="aspectFill"
class="w-full flex-1 object-cover rounded-lg" />
<view v-if="scooter.verification_status === 5"
class="absolute bottom-3 right-3 text-white text-xs px-1 rounded flex items-center"
style="background-color: #EB5305;">
<Check size="12" color="#ffffff" class="mr-0.5" />
<text class="text-white">认证</text>
</view>
</view>
<view class="flex-1 p-3 relative flex flex-col">
<view class="absolute top-2 right-2" @tap.stop="() => toggleFavorite(scooter)">
<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.brand }} {{ scooter.model }}</text>
<text class="text-xs text-gray-600 mt-1 block" style="word-break: break-all;">
{{ 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-auto">
<text class="text-orange-500 font-bold" style="font-size: 1.25rem;">
¥{{ scooter.price.toLocaleString() }}
</text>
<text class="text-xs text-gray-500 mt-1 block">{{ scooter.school_name }}</text>
</view>
</view>
</view>
</view>
</view>
</view>
<LatestScooters style="margin-bottom: 150rpx;" />
<!-- 自定义TabBar -->
<TabBar />
......@@ -125,18 +82,17 @@
import Taro, { useShareAppMessage, useDidShow, useReady } from '@tarojs/taro'
import '@tarojs/taro/html5.css' //和 nutui组件居然有冲突?
import { ref, onMounted } from 'vue'
import { Clock, Star, RectRight, Check, Search2, Shop, Heart1, HeartFill } from '@nutui/icons-vue-taro'
import { Clock, Star, Search2, Shop } from '@nutui/icons-vue-taro'
import TabBar from '@/components/TabBar.vue'
import SearchPopup from '@/components/SearchPopup.vue'
import FeaturedRecommendations from '@/components/FeaturedRecommendations.vue'
import LatestScooters from '@/components/LatestScooters.vue'
import "./index.less";
// 导入接口
import { getRecommendVehicleAPI, getVehicleListAPI } from '@/api/car';
import { useFavorite } from '@/composables/useFavorite'
import { getRecommendVehicleAPI } from '@/api/car';
import { DEFAULT_COVER_IMG } from '@/utils/config'
// 响应式数据
const searchValue = ref('')
// favoriteIds 已移除,现在使用基于对象属性的收藏模式
const showSearchPopup = ref(false)
const onSearchHandle = () => {
......@@ -147,22 +103,6 @@ const onSearchHandle = () => {
// Banner图片
const bannerImages = ref([])
// 最新上架数据
const latestScooters = ref([])
// 使用收藏功能composables
const { toggleFavorite } = useFavorite()
/**
* 点击产品卡片
* @param {Object} scooter - 电动车信息
*/
const onProductClick = (scooter) => {
Taro.navigateTo({
url: `/pages/productDetail/index?id=${scooter.id}`
})
}
/**
* 点击认证车源
*/
......@@ -181,12 +121,6 @@ const onGoodCarClick = () => {
})
}
const onNewCarClick = () => {
Taro.navigateTo({
url: '/pages/newCarList/index'
})
}
// 生命周期钩子
useDidShow(() => {
console.warn('index onShow')
......@@ -242,20 +176,6 @@ onMounted(async () => {
bannerImages.value = [DEFAULT_COVER_IMG]
}
}
// 获取最新上架
const res3 = await getVehicleListAPI({ page: 0, limit: 5 })
if (res3.code) {
latestScooters.value = res3.data.list
// 处理图片数据
const processedData = res3.data.list.map(item => ({
...item,
front_photo: item.front_photo || DEFAULT_COVER_IMG,
// 确保价格为数字类型
price: Number(item.price) || 0,
market_price: Number(item.market_price) || 0,
}))
latestScooters.value = processedData
}
})
// 分享功能
......