hookehuyr

feat(车辆管理): 实现我的车辆页面及编辑功能

- 新增我的车辆页面,展示用户车辆列表
- 添加车辆编辑和认证功能
- 实现车辆上下架操作
- 优化表单页面支持编辑模式
- 添加空状态和加载更多功能
.my-car-page {
min-height: 100vh;
background-color: #f5f5f5;
padding-bottom: 120px;
}
.car-list {
padding: 20px;
}
/* 空状态样式 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100px 40px;
text-align: center;
}
.empty-image {
width: 200px;
height: 150px;
margin-bottom: 30px;
opacity: 0.6;
}
.empty-text {
font-size: 32px;
color: #999;
margin-bottom: 40px;
}
/* 车辆卡片样式 */
.car-card {
background: white;
border-radius: 16px;
margin-bottom: 20px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
position: relative;
}
/* 状态标识 */
.status-badges {
position: absolute;
top: 20px;
right: 20px;
z-index: 2;
display: flex;
flex-direction: column;
gap: 8px;
}
.status-badge {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
border-radius: 12px;
font-size: 22px;
color: white;
&.verified {
background: linear-gradient(135deg, #10b981, #059669);
}
&.offline {
background: linear-gradient(135deg, #ef4444, #dc2626);
}
}
.status-icon {
width: 16px;
height: 16px;
}
/* 车辆图片 */
.car-image-container {
width: 100%;
height: 200px;
border-radius: 12px;
overflow: hidden;
margin-bottom: 20px;
}
.car-image {
width: 100%;
height: 100%;
object-fit: cover;
}
/* 车辆信息 */
.car-info {
margin-bottom: 20px;
}
.car-title {
font-size: 36px;
font-weight: bold;
color: #333;
margin-bottom: 12px;
}
.car-details {
display: flex;
gap: 16px;
margin-bottom: 12px;
}
.detail-item {
font-size: 26px;
color: #666;
padding: 4px 12px;
background: #f3f4f6;
border-radius: 8px;
}
.car-description {
font-size: 28px;
color: #666;
line-height: 1.5;
margin-bottom: 16px;
}
.price-section {
display: flex;
align-items: baseline;
gap: 16px;
}
.current-price {
font-size: 40px;
font-weight: bold;
color: #f97316;
}
.market-price {
font-size: 24px;
color: #999;
text-decoration: line-through;
}
/* 操作按钮 */
.action-buttons {
display: flex;
gap: 12px;
justify-content: flex-end;
}
/* 加载更多 */
.load-more {
display: flex;
justify-content: center;
padding: 40px 0;
}
.loading-text {
display: flex;
align-items: center;
gap: 12px;
font-size: 28px;
color: #666;
}
/* 没有更多数据 */
.no-more {
text-align: center;
padding: 40px 0;
font-size: 28px;
color: #999;
}
/* 响应式适配 */
@media (max-width: 750px) {
.car-list {
padding: 15px;
}
.car-card {
padding: 15px;
margin-bottom: 15px;
}
.car-title {
font-size: 32px;
}
.detail-item {
font-size: 24px;
padding: 3px 10px;
}
.car-description {
font-size: 26px;
}
.current-price {
font-size: 36px;
}
.market-price {
font-size: 22px;
}
.action-buttons {
gap: 8px;
}
.status-badge {
font-size: 20px;
padding: 3px 6px;
}
.status-icon {
width: 14px;
height: 14px;
}
}
\ No newline at end of file
<!--
* @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
* @LastEditTime: 2025-07-03 14:06:14
* @FilePath: /jgdl/src/pages/myCar/index.vue
* @Description: 文件描述
-->
<template>
<div class="red">{{ str }}</div>
<view class="my-car-page">
<!-- 车辆列表 -->
<view class="flex-1">
<!-- 滚动列表 -->
<scroll-view
class="car-list"
:style="scrollStyle"
:scroll-y="true"
@scrolltolower="loadMore"
@scroll="scroll"
:lower-threshold="50"
:enable-flex="false"
>
<!-- 空状态 -->
<view v-if="!loading && carList.length === 0" class="empty-state">
<image src="/static/images/empty-car.png" class="empty-image" mode="aspectFit" />
<text class="empty-text">暂无车源信息</text>
<nut-button color="#f97316" size="small" @click="goToSell">发布车源</nut-button>
</view>
<!-- 车辆卡片列表 -->
<view v-else class="space-y-4">
<view v-for="car in carList" :key="car.id" class="car-card">
<!-- 状态标识 -->
<view class="status-badges">
<view v-if="car.isAuthenticated" class="status-badge verified">
<Check class="status-icon" />
<text>已认证</text>
</view>
<view v-if="car.isOffline" class="status-badge offline">
<Close class="status-icon" />
<text>已下架</text>
</view>
</view>
<!-- 车辆图片 -->
<view class="car-image-container">
<image :src="car.image" class="car-image" mode="aspectFill" />
</view>
<!-- 车辆信息 -->
<view class="car-info">
<view class="car-title">{{ car.brand }} {{ car.model }}</view>
<view class="car-details">
<text class="detail-item">{{ car.year }}</text>
<text class="detail-item">{{ car.condition }}</text>
<text class="detail-item">{{ car.mileage }}公里</text>
</view>
<view class="car-description">{{ car.description }}</view>
<view class="price-section">
<view class="current-price">¥{{ car.price }}</view>
<view class="market-price">市场价 ¥{{ car.marketPrice }}</view>
</view>
</view>
<!-- 操作按钮 -->
<view class="action-buttons">
<nut-button size="small" type="default" @click="editCar(car.id)">编辑</nut-button>
<nut-button
size="small"
:type="car.isOffline ? 'success' : 'warning'"
@click="toggleOffline(car)"
>
{{ car.isOffline ? '上架' : '下架' }}
</nut-button>
<nut-button
v-if="!car.isAuthenticated"
size="small"
type="primary"
@click="authCar(car.id)"
>
认证
</nut-button>
</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 && carList.length > 0" class="no-more-container py-4 text-center">
<text class="text-gray-400 text-sm">没有更多数据了</text>
</view>
</scroll-view>
</view>
<!-- 下架确认弹窗 -->
<nut-dialog
v-model:visible="offlineDialogVisible"
title="确认操作"
:content="offlineDialogContent"
@confirm="confirmOffline"
@cancel="cancelOffline"
/>
</view>
</template>
<script setup>
// import '@tarojs/taro/html.css'
import { ref } from "vue";
import { ref, computed, onMounted } from 'vue'
import { Check, Close } from '@nutui/icons-vue-taro'
import Taro from '@tarojs/taro'
import './index.less'
// 添加样式定义
/**
* 滚动样式 - 考虑header和TabBar的高度
*/
const scrollStyle = computed(() => {
return {
height: 'calc(100vh)' // 减去header和TabBar的高度
}
})
// 页面状态
const loading = ref(false)
const hasMore = ref(true)
const currentPage = ref(1)
const pageSize = ref(10)
// 车辆列表数据
const carList = ref([])
// 下架确认弹窗
const offlineDialogVisible = ref(false)
const offlineDialogContent = ref('')
const currentOfflineCar = ref(null)
/**
* 跳转到发布车源页面
*/
const goToSell = () => {
Taro.navigateTo({
url: '/pages/sell/index'
})
}
/**
* 编辑车源
*/
const editCar = (carId) => {
Taro.navigateTo({
url: `/pages/sell/index?id=${carId}&mode=edit`
})
}
/**
* 认证车源
*/
const authCar = (carId) => {
Taro.navigateTo({
url: `/pages/setAuthCar/index?id=${carId}&mode=edit`
})
}
/**
* 切换上下架状态
*/
const toggleOffline = (car) => {
currentOfflineCar.value = car
offlineDialogContent.value = car.isOffline ? '确认要上架此车源吗?' : '确认要下架此车源吗?'
offlineDialogVisible.value = true
}
/**
* 确认上下架操作
*/
const confirmOffline = () => {
if (currentOfflineCar.value) {
const car = currentOfflineCar.value
car.isOffline = !car.isOffline
// TODO: 调用API更新车源状态
// updateCarStatus(car.id, { isOffline: car.isOffline })
Taro.showToast({
title: car.isOffline ? '已下架' : '已上架',
icon: 'success'
})
}
offlineDialogVisible.value = false
currentOfflineCar.value = null
}
/**
* 取消上下架操作
*/
const cancelOffline = () => {
offlineDialogVisible.value = false
currentOfflineCar.value = null
}
/**
* 获取车辆列表数据
*/
const fetchCarList = async (page = 1, append = false) => {
loading.value = true
try {
// 模拟API调用延迟
await new Promise(resolve => setTimeout(resolve, 800))
const mockData = generateMockCarData(page, pageSize.value)
if (append) {
carList.value.push(...mockData)
} else {
carList.value = mockData
}
// 模拟分页逻辑
if (page >= 3) {
hasMore.value = false
}
currentPage.value = page
} catch (error) {
console.error('获取车辆列表失败:', error)
showToast('加载失败,请重试', 'error')
} finally {
loading.value = false
}
}
/**
* 滚动事件处理
*/
const scroll = (e) => {
// 可以在这里处理滚动事件,比如记录滚动位置
}
/**
* 加载更多数据
*/
const loadMore = () => {
if (!hasMore.value || loading.value) return
fetchCarList(currentPage.value + 1, true)
}
/**
* 显示提示信息
*/
const showToast = (message, type = 'success') => {
Taro.showToast({
title: message,
icon: type === 'success' ? 'success' : 'none'
})
}
/**
* 生成模拟车辆数据
*/
const generateMockCarData = (page = 1, size = 10) => {
const brands = ['奔驰', '宝马', '奥迪', '大众', '丰田', '本田', '日产', '现代']
const models = ['C级', 'E级', 'S级', '3系', '5系', '7系', 'A4', 'A6', 'A8']
const conditions = ['准新车', '车况良好', '车况一般']
const images = [
'https://images.unsplash.com/photo-1549924231-f129b911e442?w=400',
'https://images.unsplash.com/photo-1552519507-da3b142c6e3d?w=400',
'https://images.unsplash.com/photo-1494976388531-d1058494cdd8?w=400',
'https://images.unsplash.com/photo-1503376780353-7e6692767b70?w=400',
'https://images.unsplash.com/photo-1525609004556-c46c7d6cf023?w=400'
]
const list = []
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 condition = conditions[Math.floor(Math.random() * conditions.length)]
const image = images[Math.floor(Math.random() * images.length)]
const price = Math.floor(Math.random() * 200000) + 50000
const marketPrice = Math.floor(price * 1.2)
const year = 2018 + Math.floor(Math.random() * 6)
const mileage = Math.floor(Math.random() * 100000) + 10000
list.push({
id: `car_${index + 1}`,
brand,
model,
year,
condition,
mileage,
price,
marketPrice,
image,
description: `${year}年${brand}${model},${condition},里程${mileage}公里`,
isAuthenticated: Math.random() > 0.5,
isOffline: Math.random() > 0.7,
publishTime: new Date(Date.now() - Math.random() * 30 * 24 * 60 * 60 * 1000).toISOString()
})
}
return list
}
// 定义响应式数据
const str = ref('Demo页面')
// 页面加载时获取数据
onMounted(() => {
fetchCarList(1, false)
})
</script>
<script>
......
......@@ -52,6 +52,12 @@
<Right size="18" color="#9ca3af" />
</view>
<view class="menu-item" @click="onSettings">
<StarN size="20" color="#6b7280" />
<text class="menu-text">我的认证</text>
<Right size="18" color="#9ca3af" />
</view>
<view class="menu-item" @click="onFeedback">
<Message size="20" color="#6b7280" />
<text class="menu-text">意见反馈</text>
......@@ -64,11 +70,6 @@
<Right size="18" color="#9ca3af" />
</view>
<view class="menu-item" @click="onSettings">
<Setting size="20" color="#6b7280" />
<text class="menu-text">设置</text>
<Right size="18" color="#9ca3af" />
</view>
</view>
<!-- 自定义TabBar -->
......@@ -79,7 +80,7 @@
<script setup>
import { ref } from 'vue'
import {
Heart, Clock, Notice, Cart, Message, Tips, Setting, Right
Heart, Clock, Notice, Cart, Message, Tips, Setting, Right, StarN
} from '@nutui/icons-vue-taro'
import Taro from '@tarojs/taro'
import TabBar from '@/components/TabBar.vue'
......
......@@ -3,7 +3,7 @@
<!-- 顶部导航 -->
<nut-config-provider :theme-vars="themeVars">
<nut-sticky top="0">
<nut-navbar title="发布车源" left-show @on-click-back="goBack">
<nut-navbar :title="isEditMode ? '编辑车源' : '发布车源'" left-show @on-click-back="goBack">
<template #left-show>
<RectLeft color="white" />
</template>
......@@ -236,7 +236,7 @@
<!-- 底部按钮 -->
<view class="bottom-actions">
<nut-button color="#f97316" size="large" block @click="onPublish">
确认发布
{{ isEditMode ? '保存修改' : '确认发布' }}
</nut-button>
</view>
......@@ -292,12 +292,18 @@
</template>
<script setup>
import { ref, reactive } from 'vue'
import { ref, reactive, onMounted } from 'vue'
import { Plus, Right, Location, RectLeft, Close } from '@nutui/icons-vue-taro'
import Taro from '@tarojs/taro'
// import BASE_URL from '@/utils/config';
import './index.less'
// 获取页面参数
const instance = Taro.getCurrentInstance()
const { id, mode } = instance.router?.params || {}
const isEditMode = ref(mode === 'edit' && id)
const carId = ref(id || '')
const themeVars = ref({
navbarBackground: '#fb923c',
navbarColor: '#ffffff',
......@@ -690,12 +696,13 @@ const onTireWearConfirm = ({ selectedValue }) => {
}
/**
* 发布车辆
* 发布/保存车辆
*/
const onPublish = () => {
if (!validateForm()) return
Taro.showLoading({ title: '发布中...' })
const loadingTitle = isEditMode.value ? '保存中...' : '发布中...'
Taro.showLoading({ title: loadingTitle })
// 收集所有上传的图片URL
const images = {
......@@ -712,21 +719,33 @@ const onPublish = () => {
imageUrls: Object.values(images).filter(url => url) // 过滤空URL
}
// 发布车辆信息数据已准备完成
if (isEditMode.value) {
submitData.id = carId.value
}
// TODO: 在此处调用实际的API接口提交数据
// console.log('提交数据:', submitData)
// if (isEditMode.value) {
// updateCar(carId.value, submitData)
// } else {
// createCar(submitData)
// }
// 模拟发布请求
// 模拟请求
setTimeout(() => {
Taro.hideLoading()
const successTitle = isEditMode.value ? '保存成功' : '发布成功'
Taro.showToast({
title: '发布成功',
title: successTitle,
icon: 'success'
})
// 发布成功后跳转到首页
// 成功后跳转
setTimeout(() => {
Taro.switchTab({ url: '/pages/index/index' })
if (isEditMode.value) {
Taro.navigateBack()
} else {
Taro.switchTab({ url: '/pages/index/index' })
}
}, 1500)
}, 2000)
}
......@@ -765,4 +784,81 @@ const validateForm = () => {
return true
}
/**
* 加载车辆数据(编辑模式)
*/
const loadCarData = async () => {
if (!isEditMode.value || !carId.value) return
try {
Taro.showLoading({ title: '加载中...' })
// TODO: 调用真实API获取车辆数据
// const carData = await getCarById(carId.value)
// 模拟API响应数据
const mockCarData = {
school: '上海理工大学',
brand: '小牛电动',
model: 'NGT',
year: '2023年',
condition: '9成新',
mileage: '1200',
range: '60',
maxSpeed: '25',
batteryCapacity: '20',
batteryWear: '轻微磨损',
brakeWear: '轻微磨损',
tireWear: '轻微磨损',
sellingPrice: '3,200',
marketPrice: '6,500',
description: '车况良好,电池续航正常,无重大事故,平时保养得当。',
images: {
front: 'https://picsum.photos/300/200?random=1',
left: 'https://picsum.photos/300/200?random=2',
right: 'https://picsum.photos/300/200?random=3',
other: 'https://picsum.photos/300/200?random=4'
}
}
// 填充表单数据
Object.assign(formData, {
school: mockCarData.school,
brand: mockCarData.brand,
model: mockCarData.model,
year: mockCarData.year,
condition: mockCarData.condition,
mileage: mockCarData.mileage,
range: mockCarData.range,
maxSpeed: mockCarData.maxSpeed,
batteryCapacity: mockCarData.batteryCapacity,
batteryWear: mockCarData.batteryWear,
brakeWear: mockCarData.brakeWear,
tireWear: mockCarData.tireWear,
sellingPrice: mockCarData.sellingPrice,
marketPrice: mockCarData.marketPrice,
description: mockCarData.description
})
// 填充图片数据
Object.assign(uploadedImages, mockCarData.images)
Taro.hideLoading()
} catch (error) {
console.error('加载车辆数据失败:', error)
Taro.hideLoading()
Taro.showToast({
title: '加载数据失败',
icon: 'none'
})
}
}
// 页面加载时执行
onMounted(() => {
if (isEditMode.value) {
loadCarData()
}
})
</script>
......
/*
* @Date: 2025-07-02 17:52:43
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-07-02 17:53:28
* @LastEditTime: 2025-07-03 14:01:51
* @FilePath: /jgdl/src/pages/setAuthCar/index.config.js
* @Description: 文件描述
*/
export default {
navigationBarTitleText: '申请认证',
navigationBarTitleText: '',
usingComponents: {
},
}
......
<!--
* @Date: 2022-09-19 14:11:06
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-07-02 18:23:27
* @LastEditTime: 2025-07-03 14:03:37
* @FilePath: /jgdl/src/pages/setAuthCar/index.vue
* @Description: 申请认证
-->
<template>
<view class="auth-car-page">
<!-- 顶部导航 -->
<nut-config-provider :theme-vars="themeVars">
<nut-sticky top="0">
<nut-navbar :title="isEditMode ? '编辑认证' : '申请认证'" left-show @on-click-back="goBack">
<template #left-show>
<RectLeft color="white" />
</template>
</nut-navbar>
</nut-sticky>
</nut-config-provider>
<!-- 表单内容 -->
<view class="form-container">
<!-- 车辆照片上传 -->
......@@ -131,7 +142,7 @@
<!-- 底部按钮 -->
<view class="bottom-actions">
<nut-button color="#f97316" size="large" block @click="onSubmit">
提交申请
{{ isEditMode ? '保存修改' : '提交申请' }}
</nut-button>
</view>
......@@ -149,11 +160,17 @@
</template>
<script setup>
import { ref, reactive } from 'vue'
import { ref, reactive, onMounted } from 'vue'
import { Plus, Right, RectLeft, Close } from '@nutui/icons-vue-taro'
import Taro from '@tarojs/taro'
import './index.less'
// 获取页面参数
const instance = Taro.getCurrentInstance()
const { id, mode } = instance.router?.params || {}
const isEditMode = ref(mode === 'edit' && id)
const carId = ref(id || '')
const themeVars = ref({
navbarBackground: '#fb923c',
navbarColor: '#ffffff',
......@@ -328,7 +345,7 @@ const onModelConfirm = (options) => {
}
/**
* 提交申请
* 提交申请/保存修改
*/
const onSubmit = () => {
// 表单验证
......@@ -380,16 +397,29 @@ const onSubmit = () => {
images: uploadedImages
}
if (isEditMode.value) {
submitData.id = carId.value
}
const loadingTitle = isEditMode.value ? '保存中' : '提交中'
Taro.showLoading({
title: '提交中',
title: loadingTitle,
mask: true
})
// TODO: 调用真实API
// if (isEditMode.value) {
// updateAuthApplication(carId.value, submitData)
// } else {
// submitAuthApplication(submitData)
// }
// 模拟提交成功
setTimeout(() => {
Taro.hideLoading()
const successTitle = isEditMode.value ? '保存成功' : '申请提交成功'
Taro.showToast({
title: '申请提交成功',
title: successTitle,
icon: 'success'
})
......@@ -398,10 +428,67 @@ const onSubmit = () => {
Taro.navigateBack()
}, 1500)
}, 2000)
}
/**
* 加载认证数据(编辑模式)
*/
const loadAuthData = async () => {
if (!isEditMode.value || !carId.value) return
try {
Taro.showLoading({ title: '加载中...' })
// TODO: 调用真实API获取认证数据
// const authData = await getAuthById(carId.value)
// 暂时不模拟数据,按用户要求
// 如果需要模拟数据,可以取消下面的注释
const mockAuthData = {
brand: '小牛电动',
model: 'NGT',
range: '60',
maxSpeed: '25',
description: '车况良好,电池续航正常,无重大事故。',
images: {
front: 'https://picsum.photos/300/200?random=5',
left: 'https://picsum.photos/300/200?random=6',
right: 'https://picsum.photos/300/200?random=7',
other: 'https://picsum.photos/300/200?random=8'
}
}
// 填充表单数据
Object.assign(formData, {
brand: mockAuthData.brand,
model: mockAuthData.model,
range: mockAuthData.range,
maxSpeed: mockAuthData.maxSpeed,
description: mockAuthData.description
})
// 实际项目中的API调用
// submitAuthApplication(submitData)
// 填充图片数据
Object.assign(uploadedImages, mockAuthData.images)
Taro.hideLoading()
} catch (error) {
console.error('加载认证数据失败:', error)
Taro.hideLoading()
Taro.showToast({
title: '加载数据失败',
icon: 'none'
})
}
}
// 页面加载时执行
onMounted(() => {
if (isEditMode.value) {
loadAuthData()
}
})
</script>
<script>
......