hookehuyr

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

添加我的关注页面,包含关注列表展示、取消关注功能及空状态处理
更新app.config.js添加新页面路由
修改个人中心页面的跳转链接
/*
* @Date: 2025-06-28 10:33:00
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-07-03 09:34:59
* @LastEditTime: 2025-07-03 12:59:10
* @FilePath: /jgdl/src/app.config.js
* @Description: 文件描述
*/
......@@ -20,6 +20,9 @@ export default {
'pages/goodCarList/index',
'pages/productDetail/index',
'pages/auth/index',
'pages/myFavorites/index',
'pages/myCar/index',
'pages/myOrders/index',
],
subpackages: [ // 配置在tabBar中的页面不能分包写到subpackages中去
{
......
/*
* @Date: 2025-07-03 12:57:44
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-07-03 12:58:03
* @FilePath: /jgdl/src/pages/myCar/index.config.js
* @Description: 文件描述
*/
export default {
navigationBarTitleText: '我卖的车',
usingComponents: {
},
}
<!--
* @Date: 2022-09-19 14:11:06
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-07-03 12:55:54
* @FilePath: /jgdl/src/pages/myFavorites/index.vue
* @Description: 文件描述
-->
<template>
<div class="red">{{ str }}</div>
</template>
<script setup>
// import '@tarojs/taro/html.css'
import { ref } from "vue";
// 定义响应式数据
const str = ref('Demo页面')
</script>
<script>
export default {
name: "demoPage",
};
</script>
export default {
navigationBarTitleText: '我的关注',
usingComponents: {
},
}
// 我的关注页面样式
.favorites-list {
.loading-container {
display: flex;
justify-content: center;
align-items: center;
padding: 32rpx 0;
.loading-text {
font-size: 28rpx;
color: #999;
}
}
.no-more-container {
display: flex;
justify-content: center;
align-items: center;
padding: 32rpx 0;
text {
font-size: 24rpx;
color: #ccc;
}
}
}
// 车辆卡片样式
.car-item {
background: white;
border-radius: 16rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
overflow: hidden;
margin-bottom: 24rpx;
.car-image {
position: relative;
.follow-tag {
position: absolute;
top: 8rpx;
right: 8rpx;
background-color: #ef4444;
color: white;
font-size: 20rpx;
padding: 4rpx 8rpx;
border-radius: 8rpx;
z-index: 2;
}
}
.car-info {
padding: 24rpx;
.car-name {
font-size: 32rpx;
font-weight: 500;
color: #333;
margin-bottom: 8rpx;
}
.car-details {
font-size: 24rpx;
color: #666;
margin-bottom: 16rpx;
}
.price-section {
display: flex;
justify-content: space-between;
align-items: center;
.price-info {
.current-price {
font-size: 32rpx;
font-weight: bold;
color: #ff6b35;
}
.original-price {
font-size: 24rpx;
color: #999;
text-decoration: line-through;
margin-left: 16rpx;
}
}
.unfollow-btn {
padding: 12rpx 24rpx;
border: 2rpx solid #ddd;
border-radius: 32rpx;
font-size: 24rpx;
color: #666;
background: white;
&:active {
background: #f5f5f5;
}
}
}
}
}
// 空状态样式
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 400rpx;
.empty-text {
font-size: 28rpx;
color: #999;
}
}
// 通用样式
.text-center {
text-align: center;
}
.py-4 {
padding-top: 32rpx;
padding-bottom: 32rpx;
}
.mb-3 {
margin-bottom: 24rpx;
}
.space-y-4 > * + * {
margin-top: 32rpx;
}
\ No newline at end of file
<!--
* @Date: 2022-09-19 14:11:06
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-07-03 13:25:46
* @FilePath: /jgdl/src/pages/myFavorites/index.vue
* @Description: 我的关注页面
-->
<template>
<view class="flex flex-col bg-white min-h-screen">
<!-- Favorites List -->
<view class="flex-1">
<!-- 滚动列表 -->
<scroll-view
class="favorites-list"
:style="scrollStyle"
:scroll-y="true"
@scrolltolower="loadMore"
@scroll="scroll"
:lower-threshold="50"
:enable-flex="false"
>
<view class="space-y-4">
<view
v-for="item in favorites"
:key="item.id"
@tap="() => onItemClick(item)"
style="border-bottom: 1px solid #e5e7eb"
>
<view class="flex p-4">
<view class="w-24 h-24 relative">
<image
:src="item.imageUrl"
:alt="item.name"
mode="aspectFill"
class="w-full h-full object-cover rounded-lg"
/>
</view>
<view class="flex-1 ml-4">
<text class="font-medium text-base block">{{ item.name }}</text>
<text class="text-sm text-gray-500 mt-1 block">{{ item.details }}</text>
<view class="mt-2 flex justify-between items-center">
<view>
<text class="text-orange-500 font-bold">
¥{{ item.price.toLocaleString() }}
</text>
<text class="text-gray-400 text-xs line-through ml-2">
¥{{ item.originalPrice.toLocaleString() }}
</text>
</view>
<nut-button
@click.stop="handleUnfollowClick(item.id)"
size="small"
type="default"
class="px-3 py-1 border border-gray-300 rounded-full text-sm"
>
取消关注
</nut-button>
</view>
</view>
</view>
</view>
</view>
<!-- Loading indicator -->
<view v-if="loading" class="loading-container py-4 text-center">
<text class="loading-text text-gray-500">加载中...</text>
</view>
<!-- 没有更多数据 -->
<view v-if="!hasMore && favorites.length > 0" class="no-more-container py-4 text-center">
<text class="text-gray-400 text-sm">没有更多数据了</text>
</view>
<!-- Empty State -->
<view
v-if="favorites.length === 0 && !loading"
class="flex flex-col items-center justify-center h-64"
>
<text class="text-gray-500">暂无关注的车辆</text>
</view>
</scroll-view>
</view>
<!-- Confirmation Modal -->
<nut-dialog
v-model:visible="showConfirmModal"
title="确认取消关注"
content="确定要取消关注此车辆吗?"
cancel-text="取消"
ok-text="确认"
@cancel="showConfirmModal = false"
@ok="confirmUnfollow"
/>
<!-- 成功提示 -->
<nut-toast
v-model:visible="toastVisible"
:msg="toastMessage"
:type="toastType"
/>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import Taro from '@tarojs/taro'
import './index.less'
// ==================== API相关 ====================
/**
* API服务 - 为真实API预留空间
*/
const apiService = {
/**
* 获取我的关注列表
* @param {number} page - 页码
* @param {number} pageSize - 每页数量
* @returns {Promise} API响应
*/
async getFavoritesList(page = 1, pageSize = 10) {
// TODO: 替换为真实API调用
// return await request.get('/api/favorites', { page, pageSize })
// 模拟API延迟
await new Promise(resolve => setTimeout(resolve, 800 + Math.random() * 400))
// 模拟API响应数据
return {
code: 200,
data: {
list: generateMockData(page, pageSize),
total: 50, // 模拟总数
hasMore: page < 5 // 模拟是否还有更多数据
},
message: 'success'
}
},
/**
* 取消关注车辆
* @param {string} carId - 车辆ID
* @returns {Promise} API响应
*/
async unfollowCar(carId) {
// TODO: 替换为真实API调用
// return await request.delete(`/api/favorites/${carId}`)
// 模拟API延迟
await new Promise(resolve => setTimeout(resolve, 500))
// 模拟API响应
return {
code: 200,
data: null,
message: '取消关注成功'
}
}
}
// ==================== 响应式数据 ====================
/**
* 我的关注列表数据
*/
const favorites = ref([])
/**
* 加载状态
*/
const loading = ref(false)
const hasMore = ref(true)
const currentPage = ref(1)
const pageSize = ref(10)
/**
* 确认弹窗显示状态
*/
const showConfirmModal = ref(false)
/**
* 当前选中要取消关注的车辆ID
*/
const selectedId = ref(null)
/**
* Toast提示
*/
const toastVisible = ref(false)
const toastMessage = ref('')
const toastType = ref('success')
/**
* 滚动样式 - 考虑header和TabBar的高度
*/
const scrollStyle = computed(() => {
return {
height: 'calc(100vh)' // 减去header和TabBar的高度
}
})
// ==================== 数据处理方法 ====================
/**
* 生成模拟数据
* @param {number} page - 页码
* @param {number} size - 每页数量
* @returns {Array} 模拟数据数组
*/
const generateMockData = (page, size) => {
const brands = ['小牛', '雅迪', '绿源', '爱玛', '台铃', '新日', '立马', '小鸟']
const models = ['豪华版', '标准版', '运动版', '经典版', '智能版', '动力版']
const images = [
'https://images.unsplash.com/photo-1558981285-6f0c94958bb6?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60',
'https://images.unsplash.com/photo-1558981403-c5f9899a28bc?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60',
'https://images.unsplash.com/photo-1591637333184-19aa84b3e01f?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60',
'https://images.unsplash.com/photo-1558980664-3a031cf67ea8?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60',
'https://images.unsplash.com/photo-1567922045116-2a00fae2ed03?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60',
'https://images.unsplash.com/photo-1573981368236-719bbb6f70f7?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60'
]
const data = []
for (let i = 0; i < size; i++) {
const index = (page - 1) * size + i
const brand = brands[Math.floor(Math.random() * brands.length)]
const model = models[Math.floor(Math.random() * models.length)]
const image = images[Math.floor(Math.random() * images.length)]
const originalPrice = Math.floor(Math.random() * 3000) + 3000
const price = Math.floor(originalPrice * (0.7 + Math.random() * 0.2)) // 7-9折
const usageTime = Math.floor(Math.random() * 24) + 1 // 1-24个月
const range = Math.floor(Math.random() * 100) + 60 // 60-160km续航
data.push({
id: `fav_${index + 100}`,
name: `${brand} ${model}`,
details: `续航${range}km | 使用${usageTime}个月`,
price: price,
originalPrice: originalPrice,
imageUrl: image,
brand: brand,
followTime: new Date(Date.now() - Math.random() * 30 * 24 * 60 * 60 * 1000).toISOString() // 最近30天内关注
})
}
return data
}
/**
* 初始化加载数据
*/
const initData = async () => {
loading.value = true
try {
const response = await apiService.getFavoritesList(1, pageSize.value)
if (response.code === 200) {
favorites.value = response.data.list
hasMore.value = response.data.hasMore
currentPage.value = 1
} else {
showToast('加载失败,请重试', 'error')
}
} catch (error) {
console.error('加载我的关注列表失败:', error)
showToast('网络错误,请重试', 'error')
} finally {
loading.value = false
}
}
/**
* 加载更多数据
*/
const loadMore = async () => {
if (loading.value || !hasMore.value) return
loading.value = true
try {
const nextPage = currentPage.value + 1
const response = await apiService.getFavoritesList(nextPage, pageSize.value)
if (response.code === 200) {
favorites.value.push(...response.data.list)
hasMore.value = response.data.hasMore
currentPage.value = nextPage
} else {
showToast('加载失败,请重试', 'error')
}
} catch (error) {
console.error('加载更多数据失败:', error)
showToast('网络错误,请重试', 'error')
} finally {
loading.value = false
}
}
// ==================== 事件处理方法 ====================
/**
* 点击车辆项目
* @param {Object} item - 车辆信息
*/
const onItemClick = (item) => {
// TODO: 跳转到车辆详情页
Taro.navigateTo({
url: `/pages/productDetail/index?id=${item.id}`
})
}
/**
* 处理取消关注点击事件
* @param {string} id - 车辆ID
*/
const handleUnfollowClick = (id) => {
selectedId.value = id
showConfirmModal.value = true
}
/**
* 确认取消关注
*/
const confirmUnfollow = async () => {
if (!selectedId.value) return
try {
const response = await apiService.unfollowCar(selectedId.value)
if (response.code === 200) {
// 从列表中移除该项
favorites.value = favorites.value.filter(item => item.id !== selectedId.value)
showToast('取消关注成功', 'success')
} else {
showToast(response.message || '取消关注失败', 'error')
}
} catch (error) {
console.error('取消关注失败:', error)
showToast('网络错误,请重试', 'error')
} finally {
showConfirmModal.value = false
selectedId.value = null
}
}
/**
* 滚动事件处理
*/
const scroll = (e) => {
// 可以在这里处理滚动事件,比如记录滚动位置
}
/**
* 显示提示信息
*/
const showToast = (message, type = 'success') => {
toastMessage.value = message
toastType.value = type
toastVisible.value = true
}
// ==================== 生命周期 ====================
onMounted(() => {
initData()
})
</script>
<script>
export default {
name: "MyFavoritesPage",
};
</script>
<style lang="less" scoped>
// 自定义样式
.object-cover {
object-fit: cover;
}
.line-through {
text-decoration: line-through;
}
.favorites-list {
.loading-container {
display: flex;
justify-content: center;
align-items: center;
.loading-text {
font-size: 28rpx;
color: #999;
}
}
.no-more-container {
display: flex;
justify-content: center;
align-items: center;
padding: 32rpx 0;
text {
font-size: 24rpx;
color: #ccc;
}
}
}
// 关注标签样式
.absolute {
position: absolute;
}
.top-1 {
top: 4rpx;
}
.right-1 {
right: 4rpx;
}
.bg-red-500 {
background-color: #ef4444;
}
.text-white {
color: white;
}
.text-xs {
font-size: 20rpx;
}
.px-1 {
padding-left: 4rpx;
padding-right: 4rpx;
}
.rounded {
border-radius: 8rpx;
}
.flex {
display: flex;
}
.items-center {
align-items: center;
}
</style>
/*
* @Date: 2025-07-03 12:57:44
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-07-03 12:58:32
* @FilePath: /jgdl/src/pages/myOrders/index.config.js
* @Description: 文件描述
*/
export default {
navigationBarTitleText: '订单管理',
usingComponents: {
},
}
<!--
* @Date: 2022-09-19 14:11:06
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-07-03 12:55:54
* @FilePath: /jgdl/src/pages/myFavorites/index.vue
* @Description: 文件描述
-->
<template>
<div class="red">{{ str }}</div>
</template>
<script setup>
// import '@tarojs/taro/html.css'
import { ref } from "vue";
// 定义响应式数据
const str = ref('Demo页面')
</script>
<script>
export default {
name: "demoPage",
};
</script>
......@@ -46,7 +46,7 @@
<Right size="18" color="#9ca3af" />
</view>
<view class="menu-item" @click="onMySoldVehicles">
<view class="menu-item" @click="onMyCar">
<Cart size="20" color="#6b7280" />
<text class="menu-text">我卖的车</text>
<Right size="18" color="#9ca3af" />
......@@ -112,7 +112,7 @@ const onEditProfile = () => {
*/
const onMyFavorites = () => {
Taro.navigateTo({
url: '/pages/my-favorites/index'
url: '/pages/myFavorites/index'
})
}
......@@ -121,7 +121,7 @@ const onMyFavorites = () => {
*/
const onOrderManagement = () => {
Taro.navigateTo({
url: '/pages/order-management/index'
url: '/pages/myOrders/index'
})
}
......@@ -137,9 +137,9 @@ const onMessages = () => {
/**
* 我卖的车
*/
const onMySoldVehicles = () => {
const onMyCar = () => {
Taro.navigateTo({
url: '/pages/my-sold-vehicles/index'
url: '/pages/myCar/index'
})
}
......@@ -148,7 +148,7 @@ const onMySoldVehicles = () => {
*/
const onHelpCenter = () => {
Taro.navigateTo({
url: '/pages/help-center/index'
url: '/pages/helpCenter/index'
})
}
......