hookehuyr

feat(我的关注): 新增我的关注页面及功能

添加我的关注页面,包含关注列表展示、取消关注功能及空状态处理
更新app.config.js添加新页面路由
修改个人中心页面的跳转链接
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 09:34:59 4 + * @LastEditTime: 2025-07-03 12:59:10
5 * @FilePath: /jgdl/src/app.config.js 5 * @FilePath: /jgdl/src/app.config.js
6 * @Description: 文件描述 6 * @Description: 文件描述
7 */ 7 */
...@@ -20,6 +20,9 @@ export default { ...@@ -20,6 +20,9 @@ export default {
20 'pages/goodCarList/index', 20 'pages/goodCarList/index',
21 'pages/productDetail/index', 21 'pages/productDetail/index',
22 'pages/auth/index', 22 'pages/auth/index',
23 + 'pages/myFavorites/index',
24 + 'pages/myCar/index',
25 + 'pages/myOrders/index',
23 ], 26 ],
24 subpackages: [ // 配置在tabBar中的页面不能分包写到subpackages中去 27 subpackages: [ // 配置在tabBar中的页面不能分包写到subpackages中去
25 { 28 {
......
1 +/*
2 + * @Date: 2025-07-03 12:57:44
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-07-03 12:58:03
5 + * @FilePath: /jgdl/src/pages/myCar/index.config.js
6 + * @Description: 文件描述
7 + */
8 +export default {
9 + navigationBarTitleText: '我卖的车',
10 + usingComponents: {
11 + },
12 +}
1 +<!--
2 + * @Date: 2022-09-19 14:11:06
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-07-03 12:55:54
5 + * @FilePath: /jgdl/src/pages/myFavorites/index.vue
6 + * @Description: 文件描述
7 +-->
8 +<template>
9 + <div class="red">{{ str }}</div>
10 +</template>
11 +
12 +<script setup>
13 +// import '@tarojs/taro/html.css'
14 +import { ref } from "vue";
15 +
16 +// 定义响应式数据
17 +const str = ref('Demo页面')
18 +</script>
19 +
20 +<script>
21 +export default {
22 + name: "demoPage",
23 +};
24 +</script>
1 +export default {
2 + navigationBarTitleText: '我的关注',
3 + usingComponents: {
4 + },
5 +}
1 +// 我的关注页面样式
2 +.favorites-list {
3 + .loading-container {
4 + display: flex;
5 + justify-content: center;
6 + align-items: center;
7 + padding: 32rpx 0;
8 +
9 + .loading-text {
10 + font-size: 28rpx;
11 + color: #999;
12 + }
13 + }
14 +
15 + .no-more-container {
16 + display: flex;
17 + justify-content: center;
18 + align-items: center;
19 + padding: 32rpx 0;
20 +
21 + text {
22 + font-size: 24rpx;
23 + color: #ccc;
24 + }
25 + }
26 +}
27 +
28 +// 车辆卡片样式
29 +.car-item {
30 + background: white;
31 + border-radius: 16rpx;
32 + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
33 + overflow: hidden;
34 + margin-bottom: 24rpx;
35 +
36 + .car-image {
37 + position: relative;
38 +
39 + .follow-tag {
40 + position: absolute;
41 + top: 8rpx;
42 + right: 8rpx;
43 + background-color: #ef4444;
44 + color: white;
45 + font-size: 20rpx;
46 + padding: 4rpx 8rpx;
47 + border-radius: 8rpx;
48 + z-index: 2;
49 + }
50 + }
51 +
52 + .car-info {
53 + padding: 24rpx;
54 +
55 + .car-name {
56 + font-size: 32rpx;
57 + font-weight: 500;
58 + color: #333;
59 + margin-bottom: 8rpx;
60 + }
61 +
62 + .car-details {
63 + font-size: 24rpx;
64 + color: #666;
65 + margin-bottom: 16rpx;
66 + }
67 +
68 + .price-section {
69 + display: flex;
70 + justify-content: space-between;
71 + align-items: center;
72 +
73 + .price-info {
74 + .current-price {
75 + font-size: 32rpx;
76 + font-weight: bold;
77 + color: #ff6b35;
78 + }
79 +
80 + .original-price {
81 + font-size: 24rpx;
82 + color: #999;
83 + text-decoration: line-through;
84 + margin-left: 16rpx;
85 + }
86 + }
87 +
88 + .unfollow-btn {
89 + padding: 12rpx 24rpx;
90 + border: 2rpx solid #ddd;
91 + border-radius: 32rpx;
92 + font-size: 24rpx;
93 + color: #666;
94 + background: white;
95 +
96 + &:active {
97 + background: #f5f5f5;
98 + }
99 + }
100 + }
101 + }
102 +}
103 +
104 +// 空状态样式
105 +.empty-state {
106 + display: flex;
107 + flex-direction: column;
108 + align-items: center;
109 + justify-content: center;
110 + height: 400rpx;
111 +
112 + .empty-text {
113 + font-size: 28rpx;
114 + color: #999;
115 + }
116 +}
117 +
118 +// 通用样式
119 +.text-center {
120 + text-align: center;
121 +}
122 +
123 +.py-4 {
124 + padding-top: 32rpx;
125 + padding-bottom: 32rpx;
126 +}
127 +
128 +.mb-3 {
129 + margin-bottom: 24rpx;
130 +}
131 +
132 +.space-y-4 > * + * {
133 + margin-top: 32rpx;
134 +}
...\ No newline at end of file ...\ No newline at end of file
1 +<!--
2 + * @Date: 2022-09-19 14:11:06
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-07-03 13:25:46
5 + * @FilePath: /jgdl/src/pages/myFavorites/index.vue
6 + * @Description: 我的关注页面
7 +-->
8 +<template>
9 + <view class="flex flex-col bg-white min-h-screen">
10 + <!-- Favorites List -->
11 + <view class="flex-1">
12 + <!-- 滚动列表 -->
13 + <scroll-view
14 + class="favorites-list"
15 + :style="scrollStyle"
16 + :scroll-y="true"
17 + @scrolltolower="loadMore"
18 + @scroll="scroll"
19 + :lower-threshold="50"
20 + :enable-flex="false"
21 + >
22 + <view class="space-y-4">
23 + <view
24 + v-for="item in favorites"
25 + :key="item.id"
26 + @tap="() => onItemClick(item)"
27 + style="border-bottom: 1px solid #e5e7eb"
28 + >
29 + <view class="flex p-4">
30 + <view class="w-24 h-24 relative">
31 + <image
32 + :src="item.imageUrl"
33 + :alt="item.name"
34 + mode="aspectFill"
35 + class="w-full h-full object-cover rounded-lg"
36 + />
37 + </view>
38 + <view class="flex-1 ml-4">
39 + <text class="font-medium text-base block">{{ item.name }}</text>
40 + <text class="text-sm text-gray-500 mt-1 block">{{ item.details }}</text>
41 + <view class="mt-2 flex justify-between items-center">
42 + <view>
43 + <text class="text-orange-500 font-bold">
44 + ¥{{ item.price.toLocaleString() }}
45 + </text>
46 + <text class="text-gray-400 text-xs line-through ml-2">
47 + ¥{{ item.originalPrice.toLocaleString() }}
48 + </text>
49 + </view>
50 + <nut-button
51 + @click.stop="handleUnfollowClick(item.id)"
52 + size="small"
53 + type="default"
54 + class="px-3 py-1 border border-gray-300 rounded-full text-sm"
55 + >
56 + 取消关注
57 + </nut-button>
58 + </view>
59 + </view>
60 + </view>
61 + </view>
62 + </view>
63 +
64 + <!-- Loading indicator -->
65 + <view v-if="loading" class="loading-container py-4 text-center">
66 + <text class="loading-text text-gray-500">加载中...</text>
67 + </view>
68 +
69 + <!-- 没有更多数据 -->
70 + <view v-if="!hasMore && favorites.length > 0" class="no-more-container py-4 text-center">
71 + <text class="text-gray-400 text-sm">没有更多数据了</text>
72 + </view>
73 +
74 + <!-- Empty State -->
75 + <view
76 + v-if="favorites.length === 0 && !loading"
77 + class="flex flex-col items-center justify-center h-64"
78 + >
79 + <text class="text-gray-500">暂无关注的车辆</text>
80 + </view>
81 + </scroll-view>
82 + </view>
83 +
84 +
85 + <!-- Confirmation Modal -->
86 + <nut-dialog
87 + v-model:visible="showConfirmModal"
88 + title="确认取消关注"
89 + content="确定要取消关注此车辆吗?"
90 + cancel-text="取消"
91 + ok-text="确认"
92 + @cancel="showConfirmModal = false"
93 + @ok="confirmUnfollow"
94 + />
95 +
96 + <!-- 成功提示 -->
97 + <nut-toast
98 + v-model:visible="toastVisible"
99 + :msg="toastMessage"
100 + :type="toastType"
101 + />
102 + </view>
103 +</template>
104 +
105 +<script setup>
106 +import { ref, computed, onMounted } from 'vue'
107 +import Taro from '@tarojs/taro'
108 +import './index.less'
109 +
110 +// ==================== API相关 ====================
111 +/**
112 + * API服务 - 为真实API预留空间
113 + */
114 +const apiService = {
115 + /**
116 + * 获取我的关注列表
117 + * @param {number} page - 页码
118 + * @param {number} pageSize - 每页数量
119 + * @returns {Promise} API响应
120 + */
121 + async getFavoritesList(page = 1, pageSize = 10) {
122 + // TODO: 替换为真实API调用
123 + // return await request.get('/api/favorites', { page, pageSize })
124 +
125 + // 模拟API延迟
126 + await new Promise(resolve => setTimeout(resolve, 800 + Math.random() * 400))
127 +
128 + // 模拟API响应数据
129 + return {
130 + code: 200,
131 + data: {
132 + list: generateMockData(page, pageSize),
133 + total: 50, // 模拟总数
134 + hasMore: page < 5 // 模拟是否还有更多数据
135 + },
136 + message: 'success'
137 + }
138 + },
139 +
140 + /**
141 + * 取消关注车辆
142 + * @param {string} carId - 车辆ID
143 + * @returns {Promise} API响应
144 + */
145 + async unfollowCar(carId) {
146 + // TODO: 替换为真实API调用
147 + // return await request.delete(`/api/favorites/${carId}`)
148 +
149 + // 模拟API延迟
150 + await new Promise(resolve => setTimeout(resolve, 500))
151 +
152 + // 模拟API响应
153 + return {
154 + code: 200,
155 + data: null,
156 + message: '取消关注成功'
157 + }
158 + }
159 +}
160 +
161 +// ==================== 响应式数据 ====================
162 +/**
163 + * 我的关注列表数据
164 + */
165 +const favorites = ref([])
166 +
167 +/**
168 + * 加载状态
169 + */
170 +const loading = ref(false)
171 +const hasMore = ref(true)
172 +const currentPage = ref(1)
173 +const pageSize = ref(10)
174 +
175 +/**
176 + * 确认弹窗显示状态
177 + */
178 +const showConfirmModal = ref(false)
179 +
180 +/**
181 + * 当前选中要取消关注的车辆ID
182 + */
183 +const selectedId = ref(null)
184 +
185 +/**
186 + * Toast提示
187 + */
188 +const toastVisible = ref(false)
189 +const toastMessage = ref('')
190 +const toastType = ref('success')
191 +
192 +/**
193 + * 滚动样式 - 考虑header和TabBar的高度
194 + */
195 +const scrollStyle = computed(() => {
196 + return {
197 + height: 'calc(100vh)' // 减去header和TabBar的高度
198 + }
199 +})
200 +
201 +// ==================== 数据处理方法 ====================
202 +/**
203 + * 生成模拟数据
204 + * @param {number} page - 页码
205 + * @param {number} size - 每页数量
206 + * @returns {Array} 模拟数据数组
207 + */
208 +const generateMockData = (page, size) => {
209 + const brands = ['小牛', '雅迪', '绿源', '爱玛', '台铃', '新日', '立马', '小鸟']
210 + const models = ['豪华版', '标准版', '运动版', '经典版', '智能版', '动力版']
211 + const images = [
212 + 'https://images.unsplash.com/photo-1558981285-6f0c94958bb6?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60',
213 + 'https://images.unsplash.com/photo-1558981403-c5f9899a28bc?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60',
214 + 'https://images.unsplash.com/photo-1591637333184-19aa84b3e01f?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60',
215 + 'https://images.unsplash.com/photo-1558980664-3a031cf67ea8?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60',
216 + 'https://images.unsplash.com/photo-1567922045116-2a00fae2ed03?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60',
217 + 'https://images.unsplash.com/photo-1573981368236-719bbb6f70f7?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60'
218 + ]
219 +
220 + const data = []
221 + for (let i = 0; i < size; i++) {
222 + const index = (page - 1) * size + i
223 + const brand = brands[Math.floor(Math.random() * brands.length)]
224 + const model = models[Math.floor(Math.random() * models.length)]
225 + const image = images[Math.floor(Math.random() * images.length)]
226 + const originalPrice = Math.floor(Math.random() * 3000) + 3000
227 + const price = Math.floor(originalPrice * (0.7 + Math.random() * 0.2)) // 7-9折
228 + const usageTime = Math.floor(Math.random() * 24) + 1 // 1-24个月
229 + const range = Math.floor(Math.random() * 100) + 60 // 60-160km续航
230 +
231 + data.push({
232 + id: `fav_${index + 100}`,
233 + name: `${brand} ${model}`,
234 + details: `续航${range}km | 使用${usageTime}个月`,
235 + price: price,
236 + originalPrice: originalPrice,
237 + imageUrl: image,
238 + brand: brand,
239 + followTime: new Date(Date.now() - Math.random() * 30 * 24 * 60 * 60 * 1000).toISOString() // 最近30天内关注
240 + })
241 + }
242 + return data
243 +}
244 +
245 +/**
246 + * 初始化加载数据
247 + */
248 +const initData = async () => {
249 + loading.value = true
250 + try {
251 + const response = await apiService.getFavoritesList(1, pageSize.value)
252 + if (response.code === 200) {
253 + favorites.value = response.data.list
254 + hasMore.value = response.data.hasMore
255 + currentPage.value = 1
256 + } else {
257 + showToast('加载失败,请重试', 'error')
258 + }
259 + } catch (error) {
260 + console.error('加载我的关注列表失败:', error)
261 + showToast('网络错误,请重试', 'error')
262 + } finally {
263 + loading.value = false
264 + }
265 +}
266 +
267 +/**
268 + * 加载更多数据
269 + */
270 +const loadMore = async () => {
271 + if (loading.value || !hasMore.value) return
272 +
273 + loading.value = true
274 + try {
275 + const nextPage = currentPage.value + 1
276 + const response = await apiService.getFavoritesList(nextPage, pageSize.value)
277 +
278 + if (response.code === 200) {
279 + favorites.value.push(...response.data.list)
280 + hasMore.value = response.data.hasMore
281 + currentPage.value = nextPage
282 + } else {
283 + showToast('加载失败,请重试', 'error')
284 + }
285 + } catch (error) {
286 + console.error('加载更多数据失败:', error)
287 + showToast('网络错误,请重试', 'error')
288 + } finally {
289 + loading.value = false
290 + }
291 +}
292 +
293 +// ==================== 事件处理方法 ====================
294 +/**
295 + * 点击车辆项目
296 + * @param {Object} item - 车辆信息
297 + */
298 +const onItemClick = (item) => {
299 + // TODO: 跳转到车辆详情页
300 + Taro.navigateTo({
301 + url: `/pages/productDetail/index?id=${item.id}`
302 + })
303 +}
304 +
305 +/**
306 + * 处理取消关注点击事件
307 + * @param {string} id - 车辆ID
308 + */
309 +const handleUnfollowClick = (id) => {
310 + selectedId.value = id
311 + showConfirmModal.value = true
312 +}
313 +
314 +/**
315 + * 确认取消关注
316 + */
317 +const confirmUnfollow = async () => {
318 + if (!selectedId.value) return
319 +
320 + try {
321 + const response = await apiService.unfollowCar(selectedId.value)
322 +
323 + if (response.code === 200) {
324 + // 从列表中移除该项
325 + favorites.value = favorites.value.filter(item => item.id !== selectedId.value)
326 + showToast('取消关注成功', 'success')
327 + } else {
328 + showToast(response.message || '取消关注失败', 'error')
329 + }
330 + } catch (error) {
331 + console.error('取消关注失败:', error)
332 + showToast('网络错误,请重试', 'error')
333 + } finally {
334 + showConfirmModal.value = false
335 + selectedId.value = null
336 + }
337 +}
338 +
339 +/**
340 + * 滚动事件处理
341 + */
342 +const scroll = (e) => {
343 + // 可以在这里处理滚动事件,比如记录滚动位置
344 +}
345 +
346 +/**
347 + * 显示提示信息
348 + */
349 +const showToast = (message, type = 'success') => {
350 + toastMessage.value = message
351 + toastType.value = type
352 + toastVisible.value = true
353 +}
354 +
355 +// ==================== 生命周期 ====================
356 +onMounted(() => {
357 + initData()
358 +})
359 +</script>
360 +
361 +<script>
362 +export default {
363 + name: "MyFavoritesPage",
364 +};
365 +</script>
366 +
367 +<style lang="less" scoped>
368 +// 自定义样式
369 +.object-cover {
370 + object-fit: cover;
371 +}
372 +
373 +.line-through {
374 + text-decoration: line-through;
375 +}
376 +
377 +.favorites-list {
378 + .loading-container {
379 + display: flex;
380 + justify-content: center;
381 + align-items: center;
382 +
383 + .loading-text {
384 + font-size: 28rpx;
385 + color: #999;
386 + }
387 + }
388 +
389 + .no-more-container {
390 + display: flex;
391 + justify-content: center;
392 + align-items: center;
393 + padding: 32rpx 0;
394 +
395 + text {
396 + font-size: 24rpx;
397 + color: #ccc;
398 + }
399 + }
400 +}
401 +
402 +// 关注标签样式
403 +.absolute {
404 + position: absolute;
405 +}
406 +
407 +.top-1 {
408 + top: 4rpx;
409 +}
410 +
411 +.right-1 {
412 + right: 4rpx;
413 +}
414 +
415 +.bg-red-500 {
416 + background-color: #ef4444;
417 +}
418 +
419 +.text-white {
420 + color: white;
421 +}
422 +
423 +.text-xs {
424 + font-size: 20rpx;
425 +}
426 +
427 +.px-1 {
428 + padding-left: 4rpx;
429 + padding-right: 4rpx;
430 +}
431 +
432 +.rounded {
433 + border-radius: 8rpx;
434 +}
435 +
436 +.flex {
437 + display: flex;
438 +}
439 +
440 +.items-center {
441 + align-items: center;
442 +}
443 +</style>
1 +/*
2 + * @Date: 2025-07-03 12:57:44
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-07-03 12:58:32
5 + * @FilePath: /jgdl/src/pages/myOrders/index.config.js
6 + * @Description: 文件描述
7 + */
8 +export default {
9 + navigationBarTitleText: '订单管理',
10 + usingComponents: {
11 + },
12 +}
1 +<!--
2 + * @Date: 2022-09-19 14:11:06
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-07-03 12:55:54
5 + * @FilePath: /jgdl/src/pages/myFavorites/index.vue
6 + * @Description: 文件描述
7 +-->
8 +<template>
9 + <div class="red">{{ str }}</div>
10 +</template>
11 +
12 +<script setup>
13 +// import '@tarojs/taro/html.css'
14 +import { ref } from "vue";
15 +
16 +// 定义响应式数据
17 +const str = ref('Demo页面')
18 +</script>
19 +
20 +<script>
21 +export default {
22 + name: "demoPage",
23 +};
24 +</script>
...@@ -46,7 +46,7 @@ ...@@ -46,7 +46,7 @@
46 <Right size="18" color="#9ca3af" /> 46 <Right size="18" color="#9ca3af" />
47 </view> 47 </view>
48 48
49 - <view class="menu-item" @click="onMySoldVehicles"> 49 + <view class="menu-item" @click="onMyCar">
50 <Cart size="20" color="#6b7280" /> 50 <Cart size="20" color="#6b7280" />
51 <text class="menu-text">我卖的车</text> 51 <text class="menu-text">我卖的车</text>
52 <Right size="18" color="#9ca3af" /> 52 <Right size="18" color="#9ca3af" />
...@@ -112,7 +112,7 @@ const onEditProfile = () => { ...@@ -112,7 +112,7 @@ const onEditProfile = () => {
112 */ 112 */
113 const onMyFavorites = () => { 113 const onMyFavorites = () => {
114 Taro.navigateTo({ 114 Taro.navigateTo({
115 - url: '/pages/my-favorites/index' 115 + url: '/pages/myFavorites/index'
116 }) 116 })
117 } 117 }
118 118
...@@ -121,7 +121,7 @@ const onMyFavorites = () => { ...@@ -121,7 +121,7 @@ const onMyFavorites = () => {
121 */ 121 */
122 const onOrderManagement = () => { 122 const onOrderManagement = () => {
123 Taro.navigateTo({ 123 Taro.navigateTo({
124 - url: '/pages/order-management/index' 124 + url: '/pages/myOrders/index'
125 }) 125 })
126 } 126 }
127 127
...@@ -137,9 +137,9 @@ const onMessages = () => { ...@@ -137,9 +137,9 @@ const onMessages = () => {
137 /** 137 /**
138 * 我卖的车 138 * 我卖的车
139 */ 139 */
140 -const onMySoldVehicles = () => { 140 +const onMyCar = () => {
141 Taro.navigateTo({ 141 Taro.navigateTo({
142 - url: '/pages/my-sold-vehicles/index' 142 + url: '/pages/myCar/index'
143 }) 143 })
144 } 144 }
145 145
...@@ -148,7 +148,7 @@ const onMySoldVehicles = () => { ...@@ -148,7 +148,7 @@ const onMySoldVehicles = () => {
148 */ 148 */
149 const onHelpCenter = () => { 149 const onHelpCenter = () => {
150 Taro.navigateTo({ 150 Taro.navigateTo({
151 - url: '/pages/help-center/index' 151 + url: '/pages/helpCenter/index'
152 }) 152 })
153 } 153 }
154 154
......