feat(首页): 新增最新上架组件并优化首页结构
将最新上架列表抽离为独立组件 LatestScooters 优化数据加载逻辑,支持分页查询和错误处理 移除首页冗余代码,保持组件单一职责
Showing
3 changed files
with
167 additions
and
85 deletions
| ... | @@ -9,6 +9,7 @@ declare module 'vue' { | ... | @@ -9,6 +9,7 @@ declare module 'vue' { |
| 9 | export interface GlobalComponents { | 9 | export interface GlobalComponents { |
| 10 | BrandModelPicker: typeof import('./src/components/BrandModelPicker.vue')['default'] | 10 | BrandModelPicker: typeof import('./src/components/BrandModelPicker.vue')['default'] |
| 11 | FeaturedRecommendations: typeof import('./src/components/FeaturedRecommendations.vue')['default'] | 11 | FeaturedRecommendations: typeof import('./src/components/FeaturedRecommendations.vue')['default'] |
| 12 | + LatestScooters: typeof import('./src/components/LatestScooters.vue')['default'] | ||
| 12 | NavBar: typeof import('./src/components/navBar.vue')['default'] | 13 | NavBar: typeof import('./src/components/navBar.vue')['default'] |
| 13 | NutActionSheet: typeof import('@nutui/nutui-taro')['ActionSheet'] | 14 | NutActionSheet: typeof import('@nutui/nutui-taro')['ActionSheet'] |
| 14 | NutButton: typeof import('@nutui/nutui-taro')['Button'] | 15 | NutButton: typeof import('@nutui/nutui-taro')['Button'] | ... | ... |
src/components/LatestScooters.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <view class="latest-scooters"> | ||
| 3 | + <!-- 最新上架 --> | ||
| 4 | + <view class="px-4 py-3"> | ||
| 5 | + <view class="flex items-center justify-between mb-3"> | ||
| 6 | + <text class="text-lg font-medium">最新上架</text> | ||
| 7 | + <view class="text-sm text-gray-500 flex items-center" @tap="onNewCarClick"> | ||
| 8 | + <text>更多</text> | ||
| 9 | + <RectRight size="12" /> | ||
| 10 | + </view> | ||
| 11 | + </view> | ||
| 12 | + <view class="space-y-4"> | ||
| 13 | + <view v-for="scooter in latestScooters" :key="scooter.id" | ||
| 14 | + class="bg-white rounded-lg shadow-sm overflow-hidden" @tap="() => onProductClick(scooter)"> | ||
| 15 | + <view class="flex min-h-32"> | ||
| 16 | + <view class="w-32 relative p-2 flex flex-col"> | ||
| 17 | + <image :src="scooter.front_photo" mode="aspectFill" | ||
| 18 | + class="w-full flex-1 object-cover rounded-lg" /> | ||
| 19 | + <view v-if="scooter.verification_status === 5" | ||
| 20 | + class="absolute bottom-3 right-3 text-white text-xs px-1 rounded flex items-center" | ||
| 21 | + style="background-color: #EB5305;"> | ||
| 22 | + <Check size="12" color="#ffffff" class="mr-0.5" /> | ||
| 23 | + <text class="text-white">认证</text> | ||
| 24 | + </view> | ||
| 25 | + </view> | ||
| 26 | + <view class="flex-1 p-3 relative flex flex-col"> | ||
| 27 | + <view class="absolute top-2 right-2" @tap.stop="() => toggleFavorite(scooter)"> | ||
| 28 | + <Heart1 v-if="!scooter.is_favorite" size="22" :color="'#9ca3af'" /> | ||
| 29 | + <HeartFill v-else size="22" :color="'#ef4444'" /> | ||
| 30 | + </view> | ||
| 31 | + <text class="font-medium text-sm block">{{ scooter.brand }} {{ scooter.model }}</text> | ||
| 32 | + <text class="text-xs text-gray-600 mt-1 block" style="word-break: break-all;"> | ||
| 33 | + {{ scooter.manufacture_year }}年 | ||
| 34 | + <text v-if="scooter.range_km">续航{{ scooter.range_km }}km</text> | ||
| 35 | + <text v-if="scooter.max_speed_kmh"> 最高时速{{ scooter.max_speed_kmh }}km/h</text> | ||
| 36 | + </text> | ||
| 37 | + <view class="mt-auto"> | ||
| 38 | + <text class="text-orange-500 font-bold" style="font-size: 1.25rem;"> | ||
| 39 | + ¥{{ scooter.price.toLocaleString() }} | ||
| 40 | + </text> | ||
| 41 | + <text class="text-xs text-gray-500 mt-1 block">{{ scooter.school_name }}</text> | ||
| 42 | + </view> | ||
| 43 | + </view> | ||
| 44 | + </view> | ||
| 45 | + </view> | ||
| 46 | + </view> | ||
| 47 | + </view> | ||
| 48 | + </view> | ||
| 49 | +</template> | ||
| 50 | + | ||
| 51 | +<script setup> | ||
| 52 | +import Taro from '@tarojs/taro' | ||
| 53 | +import { ref, onMounted } from 'vue' | ||
| 54 | +import { RectRight, Check, Heart1, HeartFill } from '@nutui/icons-vue-taro' | ||
| 55 | +import { getVehicleListAPI } from '@/api/car' | ||
| 56 | +import { useFavorite } from '@/composables/useFavorite' | ||
| 57 | +import { DEFAULT_COVER_IMG } from '@/utils/config' | ||
| 58 | + | ||
| 59 | +// 最新上架数据 | ||
| 60 | +const latestScooters = ref([]) | ||
| 61 | + | ||
| 62 | +// 使用收藏功能composables | ||
| 63 | +const { toggleFavorite } = useFavorite() | ||
| 64 | + | ||
| 65 | +/** | ||
| 66 | + * 点击产品卡片 | ||
| 67 | + * @param {Object} scooter - 电动车信息 | ||
| 68 | + */ | ||
| 69 | +const onProductClick = (scooter) => { | ||
| 70 | + Taro.navigateTo({ | ||
| 71 | + url: `/pages/productDetail/index?id=${scooter.id}` | ||
| 72 | + }) | ||
| 73 | +} | ||
| 74 | + | ||
| 75 | +/** | ||
| 76 | + * 点击查看更多 | ||
| 77 | + */ | ||
| 78 | +const onNewCarClick = () => { | ||
| 79 | + Taro.navigateTo({ | ||
| 80 | + url: '/pages/newCarList/index' | ||
| 81 | + }) | ||
| 82 | +} | ||
| 83 | + | ||
| 84 | +/** | ||
| 85 | + * 加载最新上架数据 | ||
| 86 | + * 循环查询直到获取到数据或确认没有数据为止 | ||
| 87 | + */ | ||
| 88 | +const loadLatestData = async () => { | ||
| 89 | + try { | ||
| 90 | + let page = 0 | ||
| 91 | + let hasData = false | ||
| 92 | + let allData = [] | ||
| 93 | + const limit = 5 | ||
| 94 | + | ||
| 95 | + // 循环查询直到获取到足够数据或没有更多数据 | ||
| 96 | + while (!hasData && page < 10) { // 最多查询10页防止无限循环 | ||
| 97 | + const res = await getVehicleListAPI({ page, limit }) | ||
| 98 | + | ||
| 99 | + if (res.code && res.data && res.data.list && res.data.list.length > 0) { | ||
| 100 | + // 处理图片数据 | ||
| 101 | + const processedData = res.data.list.map(item => ({ | ||
| 102 | + ...item, | ||
| 103 | + front_photo: item.front_photo || DEFAULT_COVER_IMG, | ||
| 104 | + // 确保价格为数字类型 | ||
| 105 | + price: Number(item.price) || 0, | ||
| 106 | + market_price: Number(item.market_price) || 0, | ||
| 107 | + })) | ||
| 108 | + | ||
| 109 | + allData = [...allData, ...processedData] | ||
| 110 | + | ||
| 111 | + // 如果已经获取到足够数据,停止查询 | ||
| 112 | + if (allData.length >= limit) { | ||
| 113 | + hasData = true | ||
| 114 | + latestScooters.value = allData.slice(0, limit) | ||
| 115 | + } else if (res.data.list.length < limit) { | ||
| 116 | + // 如果返回的数据少于请求的数量,说明没有更多数据了 | ||
| 117 | + hasData = true | ||
| 118 | + latestScooters.value = allData | ||
| 119 | + } | ||
| 120 | + } else { | ||
| 121 | + // 如果当前页没有数据,检查是否还有下一页 | ||
| 122 | + if (res.data && res.data.total !== undefined) { | ||
| 123 | + const totalPages = Math.ceil(res.data.total / limit) | ||
| 124 | + if (page >= totalPages - 1) { | ||
| 125 | + // 已经是最后一页,停止查询 | ||
| 126 | + break | ||
| 127 | + } | ||
| 128 | + } else { | ||
| 129 | + // 没有总数信息,如果连续没有数据就停止 | ||
| 130 | + break | ||
| 131 | + } | ||
| 132 | + } | ||
| 133 | + | ||
| 134 | + page++ | ||
| 135 | + } | ||
| 136 | + | ||
| 137 | + // 如果最终没有获取到任何数据 | ||
| 138 | + if (allData.length === 0) { | ||
| 139 | + latestScooters.value = [] | ||
| 140 | + } | ||
| 141 | + } catch (error) { | ||
| 142 | + console.error('加载最新上架数据失败:', error) | ||
| 143 | + latestScooters.value = [] | ||
| 144 | + } | ||
| 145 | +} | ||
| 146 | + | ||
| 147 | +onMounted(() => { | ||
| 148 | + loadLatestData() | ||
| 149 | +}) | ||
| 150 | + | ||
| 151 | +// 暴露刷新方法供父组件调用 | ||
| 152 | +defineExpose({ | ||
| 153 | + refresh: loadLatestData | ||
| 154 | +}) | ||
| 155 | +</script> | ||
| 156 | + | ||
| 157 | +<style lang="less" scoped> | ||
| 158 | +.latest-scooters { | ||
| 159 | + // 组件样式可以根据需要添加 | ||
| 160 | +} | ||
| 161 | +</style> |
| 1 | <!-- | 1 | <!-- |
| 2 | * @Date: 2025-06-28 10:33:00 | 2 | * @Date: 2025-06-28 10:33:00 |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | - * @LastEditTime: 2025-07-16 09:25:38 | 4 | + * @LastEditTime: 2025-07-16 09:37:40 |
| 5 | * @FilePath: /jgdl/src/pages/index/index.vue | 5 | * @FilePath: /jgdl/src/pages/index/index.vue |
| 6 | * @Description: 捡个电驴首页 | 6 | * @Description: 捡个电驴首页 |
| 7 | --> | 7 | --> |
| ... | @@ -68,50 +68,7 @@ | ... | @@ -68,50 +68,7 @@ |
| 68 | <FeaturedRecommendations /> | 68 | <FeaturedRecommendations /> |
| 69 | 69 | ||
| 70 | <!-- Latest Listings --> | 70 | <!-- Latest Listings --> |
| 71 | - <view class="px-4 mt-6 mb-20"> | 71 | + <LatestScooters style="margin-bottom: 150rpx;" /> |
| 72 | - <view class="flex justify-between items-center mb-2"> | ||
| 73 | - <text class="text-lg font-medium">最新上架</text> | ||
| 74 | - <view class="text-sm text-gray-500 flex items-center" @tap="onNewCarClick"> | ||
| 75 | - <text>更多</text> | ||
| 76 | - <RectRight size="12" /> | ||
| 77 | - </view> | ||
| 78 | - </view> | ||
| 79 | - <view class="flex flex-col"> | ||
| 80 | - <view v-for="scooter in latestScooters" :key="scooter.id" | ||
| 81 | - class="bg-white rounded-lg shadow-sm overflow-hidden mb-3" @tap="() => onProductClick(scooter)"> | ||
| 82 | - <view class="flex min-h-32"> | ||
| 83 | - <view class="w-32 relative p-2 flex flex-col"> | ||
| 84 | - <image :src="scooter.front_photo" :alt="scooter.name" mode="aspectFill" | ||
| 85 | - class="w-full flex-1 object-cover rounded-lg" /> | ||
| 86 | - <view v-if="scooter.verification_status === 5" | ||
| 87 | - class="absolute bottom-3 right-3 text-white text-xs px-1 rounded flex items-center" | ||
| 88 | - style="background-color: #EB5305;"> | ||
| 89 | - <Check size="12" color="#ffffff" class="mr-0.5" /> | ||
| 90 | - <text class="text-white">认证</text> | ||
| 91 | - </view> | ||
| 92 | - </view> | ||
| 93 | - <view class="flex-1 p-3 relative flex flex-col"> | ||
| 94 | - <view class="absolute top-2 right-2" @tap.stop="() => toggleFavorite(scooter)"> | ||
| 95 | - <Heart1 v-if="!scooter.is_favorite" size="22" :color="'#9ca3af'" /> | ||
| 96 | - <HeartFill v-else size="22" :color="'#ef4444'" /> | ||
| 97 | - </view> | ||
| 98 | - <text class="font-medium text-sm block">{{ scooter.brand }} {{ scooter.model }}</text> | ||
| 99 | - <text class="text-xs text-gray-600 mt-1 block" style="word-break: break-all;"> | ||
| 100 | - {{ scooter.manufacture_year }}年 | ||
| 101 | - <text v-if="scooter.range_km">续航{{ scooter.range_km }}km</text> | ||
| 102 | - <text v-if="scooter.max_speed_kmh"> 最高时速{{ scooter.max_speed_kmh }}km/h</text> | ||
| 103 | - </text> | ||
| 104 | - <view class="mt-auto"> | ||
| 105 | - <text class="text-orange-500 font-bold" style="font-size: 1.25rem;"> | ||
| 106 | - ¥{{ scooter.price.toLocaleString() }} | ||
| 107 | - </text> | ||
| 108 | - <text class="text-xs text-gray-500 mt-1 block">{{ scooter.school_name }}</text> | ||
| 109 | - </view> | ||
| 110 | - </view> | ||
| 111 | - </view> | ||
| 112 | - </view> | ||
| 113 | - </view> | ||
| 114 | - </view> | ||
| 115 | 72 | ||
| 116 | <!-- 自定义TabBar --> | 73 | <!-- 自定义TabBar --> |
| 117 | <TabBar /> | 74 | <TabBar /> |
| ... | @@ -125,18 +82,17 @@ | ... | @@ -125,18 +82,17 @@ |
| 125 | import Taro, { useShareAppMessage, useDidShow, useReady } from '@tarojs/taro' | 82 | import Taro, { useShareAppMessage, useDidShow, useReady } from '@tarojs/taro' |
| 126 | import '@tarojs/taro/html5.css' //和 nutui组件居然有冲突? | 83 | import '@tarojs/taro/html5.css' //和 nutui组件居然有冲突? |
| 127 | import { ref, onMounted } from 'vue' | 84 | import { ref, onMounted } from 'vue' |
| 128 | -import { Clock, Star, RectRight, Check, Search2, Shop, Heart1, HeartFill } from '@nutui/icons-vue-taro' | 85 | +import { Clock, Star, Search2, Shop } from '@nutui/icons-vue-taro' |
| 129 | import TabBar from '@/components/TabBar.vue' | 86 | import TabBar from '@/components/TabBar.vue' |
| 130 | import SearchPopup from '@/components/SearchPopup.vue' | 87 | import SearchPopup from '@/components/SearchPopup.vue' |
| 131 | import FeaturedRecommendations from '@/components/FeaturedRecommendations.vue' | 88 | import FeaturedRecommendations from '@/components/FeaturedRecommendations.vue' |
| 89 | +import LatestScooters from '@/components/LatestScooters.vue' | ||
| 132 | import "./index.less"; | 90 | import "./index.less"; |
| 133 | // 导入接口 | 91 | // 导入接口 |
| 134 | -import { getRecommendVehicleAPI, getVehicleListAPI } from '@/api/car'; | 92 | +import { getRecommendVehicleAPI } from '@/api/car'; |
| 135 | -import { useFavorite } from '@/composables/useFavorite' | ||
| 136 | import { DEFAULT_COVER_IMG } from '@/utils/config' | 93 | import { DEFAULT_COVER_IMG } from '@/utils/config' |
| 137 | // 响应式数据 | 94 | // 响应式数据 |
| 138 | const searchValue = ref('') | 95 | const searchValue = ref('') |
| 139 | -// favoriteIds 已移除,现在使用基于对象属性的收藏模式 | ||
| 140 | const showSearchPopup = ref(false) | 96 | const showSearchPopup = ref(false) |
| 141 | 97 | ||
| 142 | const onSearchHandle = () => { | 98 | const onSearchHandle = () => { |
| ... | @@ -147,22 +103,6 @@ const onSearchHandle = () => { | ... | @@ -147,22 +103,6 @@ const onSearchHandle = () => { |
| 147 | // Banner图片 | 103 | // Banner图片 |
| 148 | const bannerImages = ref([]) | 104 | const bannerImages = ref([]) |
| 149 | 105 | ||
| 150 | -// 最新上架数据 | ||
| 151 | -const latestScooters = ref([]) | ||
| 152 | - | ||
| 153 | -// 使用收藏功能composables | ||
| 154 | -const { toggleFavorite } = useFavorite() | ||
| 155 | - | ||
| 156 | -/** | ||
| 157 | - * 点击产品卡片 | ||
| 158 | - * @param {Object} scooter - 电动车信息 | ||
| 159 | - */ | ||
| 160 | -const onProductClick = (scooter) => { | ||
| 161 | - Taro.navigateTo({ | ||
| 162 | - url: `/pages/productDetail/index?id=${scooter.id}` | ||
| 163 | - }) | ||
| 164 | -} | ||
| 165 | - | ||
| 166 | /** | 106 | /** |
| 167 | * 点击认证车源 | 107 | * 点击认证车源 |
| 168 | */ | 108 | */ |
| ... | @@ -181,12 +121,6 @@ const onGoodCarClick = () => { | ... | @@ -181,12 +121,6 @@ const onGoodCarClick = () => { |
| 181 | }) | 121 | }) |
| 182 | } | 122 | } |
| 183 | 123 | ||
| 184 | -const onNewCarClick = () => { | ||
| 185 | - Taro.navigateTo({ | ||
| 186 | - url: '/pages/newCarList/index' | ||
| 187 | - }) | ||
| 188 | -} | ||
| 189 | - | ||
| 190 | // 生命周期钩子 | 124 | // 生命周期钩子 |
| 191 | useDidShow(() => { | 125 | useDidShow(() => { |
| 192 | console.warn('index onShow') | 126 | console.warn('index onShow') |
| ... | @@ -242,20 +176,6 @@ onMounted(async () => { | ... | @@ -242,20 +176,6 @@ onMounted(async () => { |
| 242 | bannerImages.value = [DEFAULT_COVER_IMG] | 176 | bannerImages.value = [DEFAULT_COVER_IMG] |
| 243 | } | 177 | } |
| 244 | } | 178 | } |
| 245 | - // 获取最新上架 | ||
| 246 | - const res3 = await getVehicleListAPI({ page: 0, limit: 5 }) | ||
| 247 | - if (res3.code) { | ||
| 248 | - latestScooters.value = res3.data.list | ||
| 249 | - // 处理图片数据 | ||
| 250 | - const processedData = res3.data.list.map(item => ({ | ||
| 251 | - ...item, | ||
| 252 | - front_photo: item.front_photo || DEFAULT_COVER_IMG, | ||
| 253 | - // 确保价格为数字类型 | ||
| 254 | - price: Number(item.price) || 0, | ||
| 255 | - market_price: Number(item.market_price) || 0, | ||
| 256 | - })) | ||
| 257 | - latestScooters.value = processedData | ||
| 258 | - } | ||
| 259 | }) | 179 | }) |
| 260 | 180 | ||
| 261 | // 分享功能 | 181 | // 分享功能 | ... | ... |
-
Please register or login to post a comment