hookehuyr

feat(search): 添加搜索页面功能

实现搜索页面基础功能,包括搜索框、筛选条件和结果展示
在首页添加搜索框跳转逻辑
更新应用配置添加搜索页面路由
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-03 20:40:08 4 + * @LastEditTime: 2025-07-04 10:34:25
5 * @FilePath: /jgdl/src/app.config.js 5 * @FilePath: /jgdl/src/app.config.js
6 - * @Description: 文件描述 6 + * @Description: 配置文件
7 */ 7 */
8 export default { 8 export default {
9 pages: [ 9 pages: [
...@@ -26,6 +26,7 @@ export default { ...@@ -26,6 +26,7 @@ export default {
26 'pages/myAuthCar/index', 26 'pages/myAuthCar/index',
27 'pages/feedBack/index', 27 'pages/feedBack/index',
28 'pages/helpCenter/index', 28 'pages/helpCenter/index',
29 + 'pages/search/index',
29 ], 30 ],
30 subpackages: [ // 配置在tabBar中的页面不能分包写到subpackages中去 31 subpackages: [ // 配置在tabBar中的页面不能分包写到subpackages中去
31 { 32 {
......
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-04 10:05:07 4 + * @LastEditTime: 2025-07-04 10:30:39
5 * @FilePath: /jgdl/src/pages/index/index.vue 5 * @FilePath: /jgdl/src/pages/index/index.vue
6 * @Description: 捡个电驴首页 6 * @Description: 捡个电驴首页
7 --> 7 -->
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
16 </nut-col> 16 </nut-col>
17 <nut-col span="18"> 17 <nut-col span="18">
18 <!-- Search Bar --> 18 <!-- Search Bar -->
19 - <nut-searchbar v-model="searchValue" placeholder="搜索品牌型号" @blur="onBlurSearch" shape="round" background="transparent" input-background="#ffffff"> 19 + <nut-searchbar v-model="searchValue" placeholder="搜索品牌型号" @focus="onFocusSearch" shape="round" background="transparent" input-background="#ffffff">
20 <template #leftin> 20 <template #leftin>
21 <Search2 /> 21 <Search2 />
22 </template> 22 </template>
...@@ -169,8 +169,11 @@ import "./index.less"; ...@@ -169,8 +169,11 @@ import "./index.less";
169 const searchValue = ref('') 169 const searchValue = ref('')
170 const favoriteIds = ref([]) 170 const favoriteIds = ref([])
171 171
172 -const onBlurSearch = () => { 172 +const onFocusSearch = () => {
173 - console.warn(searchValue.value) 173 + // 跳转到搜索页面
174 + Taro.navigateTo({
175 + url: '/pages/search/index'
176 + })
174 } 177 }
175 178
176 // Banner图片 179 // Banner图片
......
1 +/*
2 + * @Date: 2025-07-04 10:33:52
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-07-04 10:57:35
5 + * @FilePath: /jgdl/src/pages/search/index.config.js
6 + * @Description: 文件描述
7 + */
8 +export default {
9 + navigationBarTitleText: '',
10 + usingComponents: {
11 + },
12 +}
1 +/* 搜索页面样式 */
2 +.search-page {
3 + background-color: #f5f5f5;
4 + min-height: 100vh;
5 +}
6 +
7 +/* 搜索结果列表样式 */
8 +.search-results-list {
9 + width: 100%;
10 + box-sizing: border-box;
11 +
12 + /* 滚动条样式 */
13 + &::-webkit-scrollbar {
14 + width: 6rpx;
15 + }
16 +
17 + &::-webkit-scrollbar-track {
18 + background: #f1f1f1;
19 + border-radius: 3rpx;
20 + }
21 +
22 + &::-webkit-scrollbar-thumb {
23 + background: #c1c1c1;
24 + border-radius: 3rpx;
25 +
26 + &:hover {
27 + background: #a8a8a8;
28 + }
29 + }
30 +}
31 +
32 +/* 加载状态样式 */
33 +.load-more-container {
34 + padding: 40rpx 0;
35 +
36 + .loading-container {
37 + display: flex;
38 + align-items: center;
39 + justify-content: center;
40 + color: #666;
41 +
42 + .loading-spinner {
43 + width: 32rpx;
44 + height: 32rpx;
45 + border: 4rpx solid #f3f3f3;
46 + border-top: 4rpx solid #f97316;
47 + border-radius: 50%;
48 + animation: spin 1s linear infinite;
49 + margin-right: 16rpx;
50 + }
51 +
52 + .loading-text {
53 + font-size: 28rpx;
54 + color: #666;
55 + }
56 + }
57 +
58 + .no-more-data {
59 + padding: 20rpx 0;
60 + text-align: center;
61 + color: #999;
62 + font-size: 28rpx;
63 + }
64 +}
65 +
66 +/* 旋转动画 */
67 +@keyframes spin {
68 + 0% { transform: rotate(0deg); }
69 + 100% { transform: rotate(360deg); }
70 +}
71 +
72 +/* 空状态样式 */
73 +.empty-state {
74 + .empty-icon {
75 + display: flex;
76 + justify-content: center;
77 + align-items: center;
78 + opacity: 0.6;
79 + }
80 +}
81 +
82 +.no-results-state {
83 + .no-results-icon {
84 + display: flex;
85 + justify-content: center;
86 + align-items: center;
87 + opacity: 0.7;
88 + }
89 +}
90 +
91 +/* 搜索结果卡片样式优化 */
92 +.search-results-list {
93 + .grid {
94 + .bg-white {
95 + transition: all 0.3s ease;
96 + border: 1rpx solid #e5e7eb;
97 +
98 + &:hover {
99 + box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.12);
100 + transform: translateY(-2rpx);
101 + }
102 + }
103 + }
104 +}
1 +<!--
2 + * @Date: 2022-09-19 14:11:06
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-07-04 11:12:55
5 + * @FilePath: /jgdl/src/pages/search/index.vue
6 + * @Description: 搜索页面
7 +-->
8 +<template>
9 + <view class="search-page">
10 + <view class="flex flex-col bg-white min-h-screen">
11 + <!-- Header -->
12 + <nut-sticky>
13 + <view class="bg-orange-400 p-4 pt-4 pb-4">
14 + <nut-row type="flex" justify="center" align="center">
15 + <nut-col span="3">
16 + <view class="text-xl font-bold text-white">搜索</view>
17 + </nut-col>
18 + <nut-col span="21">
19 + <!-- Search Bar -->
20 + <nut-searchbar v-model="searchValue" placeholder="搜索品牌型号" @search="onSearch" @blur="onBlurSearch" @clear="onClearSearch" shape="round" background="transparent" input-background="#ffffff">
21 + <template #leftin>
22 + <Search2 />
23 + </template>
24 + </nut-searchbar>
25 + </nut-col>
26 + </nut-row>
27 + </view>
28 +
29 + <!-- Filter options -->
30 + <nut-menu>
31 + <nut-menu-item v-model="selectedBrand" :options="brandOptions" @change="onBrandChange" />
32 + <nut-menu-item v-model="selectedYear" :options="yearOptions" @change="onYearChange" />
33 + <nut-menu-item v-model="selectedSchool" :options="schoolOptions" @change="onSchoolChange" />
34 + </nut-menu>
35 + </nut-sticky>
36 +
37 + <!-- Search Results -->
38 + <view class="flex-1">
39 + <!-- 滚动列表 -->
40 + <scroll-view
41 + class="search-results-list"
42 + :style="scrollStyle"
43 + :scroll-y="true"
44 + @scrolltolower="loadMore"
45 + :lower-threshold="50"
46 + :enable-flex="false"
47 + >
48 + <view class="p-4">
49 + <!-- 搜索结果统计 -->
50 + <view v-if="searchResults.length > 0" class="mb-4">
51 + <text class="text-sm text-gray-600">找到 {{ totalCount }} 个相关结果</text>
52 + </view>
53 +
54 + <!-- 搜索结果网格布局 -->
55 + <view class="grid grid-cols-2 gap-3">
56 + <view v-for="scooter in searchResults" :key="scooter.id"
57 + class="bg-white rounded-lg shadow-sm overflow-hidden" @tap="() => onProductClick(scooter)">
58 + <view class="relative p-2">
59 + <image :src="scooter.imageUrl" :alt="scooter.name" mode="aspectFill"
60 + class="w-full h-36 object-cover rounded-lg" />
61 + <view class="absolute top-4 right-3 w-7 h-7 rounded-full bg-white bg-opacity-90" @tap.stop="() => toggleFavorite(scooter.id)" style="padding-top: 12rpx; padding-left: 10rpx;">
62 + <Heart1 v-if="!favoriteIds.includes(scooter.id)" size="22" :color="'#9ca3af'" />
63 + <HeartFill v-else size="22" :color="'#ef4444'" />
64 + </view>
65 + <view v-if="scooter.isVerified"
66 + class="absolute bottom-4 right-4 text-white text-xs px-1.5 py-0.5 rounded flex items-center" style="background-color: #EB5305;">
67 + <Check size="12" color="#ffffff" class="mr-0.5" />
68 + <text class="text-white">认证</text>
69 + </view>
70 + </view>
71 + <view class="p-2 pl-3">
72 + <text class="font-medium text-sm block">{{ scooter.name }}</text>
73 + <text class="text-xs text-gray-500 block mt-1 mb-1">
74 + {{ scooter.year }} · {{ scooter.school }}
75 + <text v-if="scooter.batteryHealth">电池健康度{{ scooter.batteryHealth }}%</text>
76 + <text v-if="scooter.mileage"> 行驶{{ scooter.mileage }}公里</text>
77 + </text>
78 + <view class="mt-1">
79 + <text class="text-orange-500 font-bold" style="font-size: 1.25rem;">
80 + ¥{{ scooter.price.toLocaleString() }}
81 + </text>
82 + </view>
83 + </view>
84 + </view>
85 + </view>
86 +
87 + <!-- 初始空状态 -->
88 + <view v-if="searchResults.length === 0 && !loading && !hasSearched" class="empty-state text-center py-20">
89 + <view class="empty-icon mb-4">
90 + <Search2 size="80" color="#d1d5db" />
91 + </view>
92 + <text class="text-lg font-medium text-gray-600 block mb-2">搜索电动车</text>
93 + <text class="text-sm text-gray-400 block">输入品牌型号,找到心仪的电动车</text>
94 + </view>
95 +
96 + <!-- 搜索无结果状态 -->
97 + <view v-if="searchResults.length === 0 && !loading && hasSearched" class="no-results-state text-center py-20">
98 + <view class="no-results-icon mb-4">
99 + <Search2 size="60" color="#9ca3af" />
100 + </view>
101 + <text class="text-base font-medium text-gray-600 block mb-2">暂无搜索结果</text>
102 + <text class="text-sm text-gray-400 block">试试其他关键词或调整筛选条件</text>
103 + </view>
104 +
105 + <!-- 加载更多 -->
106 + <view v-if="searchResults.length > 0" class="load-more-container mt-6">
107 + <view v-if="loading" class="loading-container">
108 + <view class="loading-spinner"></view>
109 + <text class="loading-text">加载中...</text>
110 + </view>
111 + <view v-else-if="noMoreData" class="no-more-data text-center">
112 + <text class="text-gray-500">没有更多数据了</text>
113 + </view>
114 + </view>
115 + </view>
116 + </scroll-view>
117 + </view>
118 + </view>
119 + </view>
120 +</template>
121 +
122 +<script setup>
123 +import { ref, computed, onMounted } from 'vue'
124 +import Taro from '@tarojs/taro'
125 +import { Search2, Check, Heart1, HeartFill } from '@nutui/icons-vue-taro'
126 +import "./index.less";
127 +
128 +// 响应式数据
129 +const searchValue = ref('')
130 +const favoriteIds = ref(['5', '7', '1'])
131 +const hasSearched = ref(false)
132 +
133 +// 滚动相关
134 +const loading = ref(false)
135 +const noMoreData = ref(false)
136 +const currentPage = ref(1)
137 +const pageSize = ref(10)
138 +const totalCount = ref(0)
139 +
140 +// Filter states
141 +const selectedBrand = ref('全部品牌')
142 +const selectedYear = ref('出厂年份')
143 +const selectedSchool = ref('所在学校')
144 +
145 +// Menu选项数据
146 +const brandOptions = ref([
147 + { text: '全部品牌', value: '全部品牌' },
148 + { text: '雅迪', value: '雅迪' },
149 + { text: '台铃', value: '台铃' },
150 + { text: '小鸟', value: '小鸟' },
151 + { text: '新日', value: '新日' },
152 + { text: '爱玛', value: '爱玛' },
153 + { text: '小牛', value: '小牛' }
154 +])
155 +
156 +const yearOptions = ref([
157 + { text: '出厂年份', value: '出厂年份' },
158 + { text: '2024年', value: '2024年' },
159 + { text: '2023年', value: '2023年' },
160 + { text: '2022年', value: '2022年' },
161 + { text: '2021年', value: '2021年' },
162 + { text: '2020年', value: '2020年' }
163 +])
164 +
165 +const schoolOptions = ref([
166 + { text: '所在学校', value: '所在学校' },
167 + { text: '上海理工大学', value: '上海理工大学' },
168 + { text: '上海复旦大学', value: '上海复旦大学' },
169 + { text: '上海同济大学', value: '上海同济大学' },
170 + { text: '上海交通大学', value: '上海交通大学' }
171 +])
172 +
173 +// 搜索结果数据
174 +const searchResults = ref([])
175 +
176 +// 滚动样式
177 +const scrollStyle = computed(() => {
178 + return {
179 + height: 'calc(100vh - 200rpx)'
180 + }
181 +})
182 +
183 +/**
184 + * 生成模拟数据
185 + * @param {number} page 页码
186 + * @param {number} size 每页数量
187 + * @returns {Array} 模拟数据数组
188 + */
189 +const generateMockData = (page, size) => {
190 + const brands = ['雅迪', '台铃', '小鸟', '新日', '爱玛', '小牛']
191 + const schools = ['上海理工大学', '上海复旦大学', '上海同济大学', '上海交通大学']
192 + const years = ['2024年', '2023年', '2022年', '2021年', '2020年']
193 + const images = [
194 + 'https://images.unsplash.com/photo-1558981285-6f0c94958bb6?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60',
195 + 'https://images.unsplash.com/photo-1558981403-c5f9899a28bc?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60',
196 + 'https://images.unsplash.com/photo-1591637333184-19aa84b3e01f?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60',
197 + 'https://images.unsplash.com/photo-1558980664-3a031cf67ea8?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60',
198 + 'https://images.unsplash.com/photo-1595941069915-4ebc5197c14a?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60'
199 + ]
200 +
201 + const data = []
202 + const startId = (page - 1) * size + 1
203 +
204 + for (let i = 0; i < size; i++) {
205 + const brand = brands[Math.floor(Math.random() * brands.length)]
206 + const school = schools[Math.floor(Math.random() * schools.length)]
207 + const year = years[Math.floor(Math.random() * years.length)]
208 + const image = images[Math.floor(Math.random() * images.length)]
209 +
210 + data.push({
211 + id: `search_${startId + i}`,
212 + name: `${brand} ${['豪华版', '标准版', '运动版', '经典版'][Math.floor(Math.random() * 4)]}`,
213 + year: year,
214 + school: school,
215 + price: Math.floor(Math.random() * 5000) + 2000,
216 + imageUrl: image,
217 + batteryHealth: Math.floor(Math.random() * 30) + 70,
218 + mileage: Math.floor(Math.random() * 5000) + 500,
219 + brand: brand,
220 + isVerified: Math.random() > 0.6
221 + })
222 + }
223 +
224 + return data
225 +}
226 +
227 +/**
228 + * 执行搜索
229 + */
230 +const performSearch = () => {
231 + if (!searchValue.value.trim()) {
232 + clearSearchResults()
233 + return
234 + }
235 +
236 + loading.value = true
237 + hasSearched.value = true
238 + currentPage.value = 1
239 + noMoreData.value = false
240 +
241 + // 模拟API调用延迟
242 + setTimeout(() => {
243 + const mockData = generateMockData(1, pageSize.value)
244 + searchResults.value = mockData
245 + totalCount.value = 50 // 模拟总数
246 + loading.value = false
247 + }, 500)
248 +}
249 +
250 +/**
251 + * 清除搜索结果
252 + */
253 +const clearSearchResults = () => {
254 + searchResults.value = []
255 + hasSearched.value = false
256 + currentPage.value = 1
257 + noMoreData.value = false
258 + totalCount.value = 0
259 +}
260 +
261 +/**
262 + * 搜索事件处理
263 + */
264 +const onSearch = () => {
265 + performSearch()
266 +}
267 +
268 +/**
269 + * 搜索框失焦事件
270 + */
271 +const onBlurSearch = () => {
272 + if (searchValue.value.trim()) {
273 + performSearch()
274 + } else {
275 + // 如果搜索框为空,清除搜索结果
276 + clearSearchResults()
277 + }
278 +}
279 +
280 +const onClearSearch = () => {
281 + clearSearchResults()
282 +}
283 +
284 +/**
285 + * 品牌筛选变化
286 + */
287 +const onBrandChange = () => {
288 + if (hasSearched.value) {
289 + performSearch()
290 + }
291 +}
292 +
293 +/**
294 + * 年份筛选变化
295 + */
296 +const onYearChange = () => {
297 + if (hasSearched.value) {
298 + performSearch()
299 + }
300 +}
301 +
302 +/**
303 + * 学校筛选变化
304 + */
305 +const onSchoolChange = () => {
306 + if (hasSearched.value) {
307 + performSearch()
308 + }
309 +}
310 +
311 +/**
312 + * 加载更多数据
313 + */
314 +const loadMore = () => {
315 + if (loading.value || noMoreData.value) return
316 +
317 + loading.value = true
318 + currentPage.value++
319 +
320 + // 模拟API调用延迟
321 + setTimeout(() => {
322 + const mockData = generateMockData(currentPage.value, pageSize.value)
323 +
324 + if (mockData.length === 0 || searchResults.value.length >= totalCount.value) {
325 + noMoreData.value = true
326 + } else {
327 + searchResults.value.push(...mockData)
328 + }
329 +
330 + loading.value = false
331 + }, 500)
332 +}
333 +
334 +/**
335 + * 切换收藏状态
336 + * @param {string} id 商品ID
337 + */
338 +const toggleFavorite = (id) => {
339 + const index = favoriteIds.value.indexOf(id)
340 + if (index > -1) {
341 + favoriteIds.value.splice(index, 1)
342 + } else {
343 + favoriteIds.value.push(id)
344 + }
345 +}
346 +
347 +/**
348 + * 商品点击事件
349 + * @param {Object} scooter 商品信息
350 + */
351 +const onProductClick = (scooter) => {
352 + Taro.navigateTo({
353 + url: `/pages/productDetail/index?id=${scooter.id}`
354 + })
355 +}
356 +
357 +// 页面初始化
358 +onMounted(() => {
359 + // 如果有搜索参数,自动执行搜索
360 + const instance = Taro.getCurrentInstance()
361 + if (instance.router?.params?.keyword) {
362 + searchValue.value = instance.router.params.keyword
363 + performSearch()
364 + }
365 +})
366 +</script>
367 +
368 +<script>
369 +export default {
370 + name: "SearchPage",
371 +};
372 +</script>