hookehuyr

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

将最新上架列表抽离为独立组件 LatestScooters
优化数据加载逻辑,支持分页查询和错误处理
移除首页冗余代码,保持组件单一职责
...@@ -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']
......
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 // 分享功能
......