hookehuyr

feat: 添加电动车交易平台核心功能

- 新增首页、分类、卖车、消息和个人中心五个主要页面
- 实现首页商品展示、搜索和分类功能
- 添加卖车表单页面,支持图片上传和基本信息填写
- 创建消息和个人中心页面,完善用户交互
- 配置底部导航栏和页面路由
- 优化UI样式,修复NutUI图标字体问题
- 添加多套SVG图标资源
- 更新.gitignore忽略.resource文件
......@@ -7,3 +7,4 @@ node_modules/
.swc
.history
.trae
.resource
......
......@@ -8,8 +8,7 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
NavBar: typeof import('./src/components/navBar.vue')['default']
NutButton: typeof import('@nutui/nutui-taro')['Button']
NutToast: typeof import('@nutui/nutui-taro')['Toast']
NutInput: typeof import('@nutui/nutui-taro')['Input']
Picker: typeof import('./src/components/time-picker-data/picker.vue')['default']
PosterBuilder: typeof import('./src/components/PosterBuilder/index.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
......
/*
* @Date: 2025-06-28 10:33:00
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-06-28 11:05:47
* @FilePath: /myApp/src/app.config.js
* @LastEditTime: 2025-07-01 17:55:25
* @FilePath: /jgdl/src/app.config.js
* @Description: 文件描述
*/
export default {
pages: [
'pages/index/index',
'pages/post/index',
'pages/sell/index',
'pages/messages/index',
'pages/profile/index',
'pages/auth/index',
],
subpackages: [ // 配置在tabBar中的页面不能分包写到subpackages中去
......@@ -21,5 +25,43 @@ export default {
navigationBarBackgroundColor: '#fff',
navigationBarTitleText: 'WeChat',
navigationBarTextStyle: 'black'
},
tabBar: {
color: '#6b7280',
selectedColor: '#f97316',
backgroundColor: '#ffffff',
borderStyle: 'black',
list: [
{
pagePath: 'pages/index/index',
text: '首页',
iconPath: 'assets/images/icon/icon_home1@2x.png',
selectedIconPath: 'assets/images/icon/icon_home2@2x.png'
},
{
pagePath: 'pages/post/index',
text: '分类',
iconPath: 'assets/images/icon/icon_book1@2x.png',
selectedIconPath: 'assets/images/icon/icon_book2@2x.png'
},
{
pagePath: 'pages/sell/index',
text: '我要卖车',
iconPath: 'assets/images/icon/icon_server1.png',
selectedIconPath: 'assets/images/icon/icon_server2.png'
},
{
pagePath: 'pages/messages/index',
text: '消息',
iconPath: 'assets/images/icon/icon_book1@2x.png',
selectedIconPath: 'assets/images/icon/icon_book2@2x.png'
},
{
pagePath: 'pages/profile/index',
text: '我的',
iconPath: 'assets/images/icon/icon_my1@2x.png',
selectedIconPath: 'assets/images/icon/icon_my2@2x.png'
}
]
}
}
......
@tailwind base;
@tailwind components;
@tailwind utilities;
/* 修复 NutUI 图标字体样式 */
.nut-icon {
font-style: normal !important;
font-weight: normal !important;
}
/* 修复所有可能的图标字体 */
[class*="nut-icon"] {
font-style: normal !important;
font-weight: normal !important;
}
......
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 3H8V8H3V3Z" stroke="#f97316" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="#fef7ed"/>
<path d="M16 3H21V8H16V3Z" stroke="#f97316" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="#fef7ed"/>
<path d="M16 16H21V21H16V16Z" stroke="#f97316" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="#fef7ed"/>
<path d="M3 16H8V21H3V16Z" stroke="#f97316" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="#fef7ed"/>
</svg>
\ No newline at end of file
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 3H8V8H3V3Z" stroke="#6b7280" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M16 3H21V8H16V3Z" stroke="#6b7280" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M16 16H21V21H16V16Z" stroke="#6b7280" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 16H8V21H3V16Z" stroke="#6b7280" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
\ No newline at end of file
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 9L12 2L21 9V20C21 20.5304 20.7893 21.0391 20.4142 21.4142C20.0391 21.7893 19.5304 22 19 22H5C4.46957 22 3.96086 21.7893 3.58579 21.4142C3.21071 21.0391 3 20.5304 3 20V9Z" stroke="#f97316" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="#fef7ed"/>
<path d="M9 22V12H15V22" stroke="#f97316" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
\ No newline at end of file
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 9L12 2L21 9V20C21 20.5304 20.7893 21.0391 20.4142 21.4142C20.0391 21.7893 19.5304 22 19 22H5C4.46957 22 3.96086 21.7893 3.58579 21.4142C3.21071 21.0391 3 20.5304 3 20V9Z" stroke="#6b7280" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 22V12H15V22" stroke="#6b7280" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
\ No newline at end of file
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21 15C21 15.5304 20.7893 16.0391 20.4142 16.4142C20.0391 16.7893 19.5304 17 19 17H7L3 21V5C3 4.46957 3.21071 3.96086 3.58579 3.58579C3.96086 3.21071 4.46957 3 5 3H19C19.5304 3 20.0391 3.21071 20.4142 3.58579C20.7893 3.96086 21 4.46957 21 5V15Z" stroke="#f97316" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="#fef7ed"/>
</svg>
\ No newline at end of file
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21 15C21 15.5304 20.7893 16.0391 20.4142 16.4142C20.0391 16.7893 19.5304 17 19 17H7L3 21V5C3 4.46957 3.21071 3.96086 3.58579 3.58579C3.96086 3.21071 4.46957 3 5 3H19C19.5304 3 20.0391 3.21071 20.4142 3.58579C20.7893 3.96086 21 4.46957 21 5V15Z" stroke="#6b7280" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
\ No newline at end of file
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 21V19C20 17.9391 19.5786 16.9217 18.8284 16.1716C18.0783 15.4214 17.0609 15 16 15H8C6.93913 15 5.92172 15.4214 5.17157 16.1716C4.42143 16.9217 4 17.9391 4 19V21" stroke="#f97316" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="12" cy="7" r="4" stroke="#f97316" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="#fef7ed"/>
</svg>
\ No newline at end of file
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 21V19C20 17.9391 19.5786 16.9217 18.8284 16.1716C18.0783 15.4214 17.0609 15 16 15H8C6.93913 15 5.92172 15.4214 5.17157 16.1716C4.42143 16.9217 4 17.9391 4 19V21" stroke="#6b7280" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="12" cy="7" r="4" stroke="#6b7280" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
\ No newline at end of file
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" stroke="#f97316" stroke-width="2" fill="#fef7ed"/>
<line x1="12" y1="8" x2="12" y2="16" stroke="#f97316" stroke-width="2" stroke-linecap="round"/>
<line x1="8" y1="12" x2="16" y2="12" stroke="#f97316" stroke-width="2" stroke-linecap="round"/>
</svg>
\ No newline at end of file
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" stroke="#6b7280" stroke-width="2"/>
<line x1="12" y1="8" x2="12" y2="16" stroke="#6b7280" stroke-width="2" stroke-linecap="round"/>
<line x1="8" y1="12" x2="16" y2="12" stroke="#6b7280" stroke-width="2" stroke-linecap="round"/>
</svg>
\ No newline at end of file
/**
* index页面样式
* 捡个电驴首页样式
*/
.index {
padding: 20px;
.nut-button {
margin-bottom: 20px;
}
/* 搜索框样式 */
.nut-input {
--nut-input-border-radius: 9999px;
--nut-input-padding: 8px 16px 8px 40px;
--nut-input-font-size: 14px;
--nut-input-background-color: #ffffff;
--nut-input-border-color: transparent;
}
/* 网格布局修复 */
.grid {
display: grid;
}
.grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
/* 间距修复 */
.gap-3 {
gap: 12px;
}
.space-x-1 > * + * {
margin-left: 4px;
}
/* 阴影效果 */
.shadow-sm {
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
}
/* 图片样式 */
image {
display: block;
width: 100%;
height: 100%;
}
/* 文本省略 */
.text-ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 多行文本省略 */
.line-clamp-2 {
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
/* 修复flex布局在小程序中的问题 */
.flex {
display: flex;
}
.flex-col {
flex-direction: column;
}
.items-center {
align-items: center;
}
.justify-center {
justify-content: center;
}
.justify-between {
justify-content: space-between;
}
.justify-around {
justify-content: space-around;
}
/* 响应式图片 */
.aspect-fill {
object-fit: cover;
}
/* 卡片悬停效果 */
.card-hover {
transition: transform 0.2s ease-in-out;
}
.card-hover:active {
transform: scale(0.98);
}
\ No newline at end of file
......
<!--
* @Date: 2025-06-28 10:33:00
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-07-01 11:13:13
* @FilePath: /myApp/src/pages/index/index.vue
* @Description: 文件描述
* @LastEditTime: 2025-07-01 18:18:19
* @FilePath: /jgdl/src/pages/index/index.vue
* @Description: 捡个电驴首页
-->
<template>
<view class="index">
<nut-button type="primary" @click="onClick">按钮</nut-button>
<nut-toast v-model:visible="show" msg="你成功了" />
<View className="text-[#acc855] text-[100px]">Hello world!</View>
<view class="flex flex-col bg-orange-50 min-h-screen">
<!-- Header -->
<view class="bg-orange-400 p-4 pt-8 pb-6">
<view class="text-2xl font-bold text-white mb-3">捡个电驴</view>
<!-- Search Bar -->
<view class="relative">
<view class="absolute inset-y-0 left-3 flex items-center pointer-events-none">
<Search size="18" color="#9ca3af" />
</view>
<nut-input
v-model="searchValue"
placeholder="搜索品牌型号"
class="w-full py-2 pl-10 pr-4 rounded-full bg-white"
:border="false"
/>
</view>
</view>
<!-- Banner -->
<view class="px-4 mt-4">
<view class="relative rounded-lg overflow-hidden">
<image
:src="bannerImages[0]"
mode="aspectFill"
class="w-full h-40 object-cover rounded-lg"
/>
<view class="absolute bottom-2 right-2 flex space-x-1">
<view class="w-2 h-2 bg-white rounded-full opacity-100"></view>
<view class="w-2 h-2 bg-white rounded-full opacity-50"></view>
<view class="w-2 h-2 bg-white rounded-full opacity-50"></view>
</view>
</view>
</view>
<!-- Category Icons -->
<view class="px-4 mt-2">
<view class="flex justify-around py-4">
<view class="flex flex-col items-center">
<view class="w-12 h-12 rounded-full bg-orange-100 flex items-center justify-center">
<Clock size="20" color="#f97316" />
</view>
<text class="text-xs mt-1 text-gray-700">最新上架</text>
</view>
<view class="flex flex-col items-center">
<view class="w-12 h-12 rounded-full bg-orange-100 flex items-center justify-center">
<Star size="20" color="#f97316" />
</view>
<text class="text-xs mt-1 text-gray-700">特价好车</text>
</view>
<view class="flex flex-col items-center" @tap="onCertifiedClick">
<view class="w-12 h-12 rounded-full bg-orange-100 flex items-center justify-center">
<Service size="20" color="#f97316" />
</view>
<text class="text-xs mt-1 text-gray-700">认证车源</text>
</view>
</view>
</view>
<!-- Featured Recommendations -->
<view class="px-4 mt-4">
<view class="flex justify-between items-center mb-2">
<text class="text-lg font-medium">精品推荐</text>
<view class="text-sm text-gray-500 flex items-center">
<text>更多</text>
<Right size="16" />
</view>
</view>
<view class="grid grid-cols-2 gap-3">
<view
v-for="scooter in featuredScooters"
:key="scooter.id"
class="bg-white rounded-lg shadow-sm overflow-hidden"
@tap="() => onProductClick(scooter)"
>
<view class="relative p-2">
<image
:src="scooter.imageUrl"
:alt="scooter.name"
mode="aspectFill"
class="w-full h-36 object-cover rounded-lg"
/>
<view
class="absolute top-4 right-4 p-1"
@tap.stop="() => toggleFavorite(scooter.id)"
>
<Heart
size="24"
:color="favoriteIds.includes(scooter.id) ? '#f97316' : '#ffffff'"
:fill="favoriteIds.includes(scooter.id) ? '#f97316' : 'none'"
/>
</view>
<view
v-if="scooter.isVerified"
class="absolute bottom-4 right-4 bg-orange-500 text-white text-xs px-1.5 py-0.5 rounded flex items-center"
>
<Check size="12" color="#ffffff" class="mr-0.5" />
<text class="text-white">认证</text>
</view>
</view>
<view class="p-2">
<text class="font-medium text-sm block">{{ scooter.name }}</text>
<text class="text-xs text-gray-500 block">
{{ scooter.year }} ·
<text v-if="scooter.batteryHealth">电池健康度{{ scooter.batteryHealth }}%</text>
<text v-if="scooter.mileage"> 行驶{{ scooter.mileage }}公里</text>
</text>
<view class="mt-1">
<text class="text-orange-500 font-bold">
¥{{ scooter.price.toLocaleString() }}
</text>
<text
v-if="scooter.isVerified"
class="ml-2 text-xs px-1 py-0.5 bg-orange-100 text-orange-500 rounded"
>
低于市场价10%
</text>
<text class="text-xs text-gray-500 mt-1 block">{{ scooter.school }}</text>
</view>
</view>
</view>
</view>
</view>
<!-- Latest Listings -->
<view class="px-4 mt-6 mb-20">
<view class="flex justify-between items-center mb-2">
<text class="text-lg font-medium">最新上架</text>
<view class="text-sm text-gray-500 flex items-center">
<text>更多</text>
<Right size="16" />
</view>
</view>
<view class="flex flex-col">
<view
v-for="scooter in latestScooters"
:key="scooter.id"
class="bg-white rounded-lg shadow-sm overflow-hidden mb-3"
@tap="() => onProductClick(scooter)"
>
<view class="flex">
<view class="w-32 h-24 relative p-2">
<image
:src="scooter.imageUrl"
:alt="scooter.name"
mode="aspectFill"
class="w-full h-full object-cover rounded-lg"
/>
<view
v-if="scooter.isVerified"
class="absolute bottom-3 right-3 bg-orange-500 text-white text-xs px-1 rounded flex items-center"
>
<Check size="12" color="#ffffff" class="mr-0.5" />
<text class="text-white">认证</text>
</view>
</view>
<view class="flex-1 p-3 relative">
<view
class="absolute top-2 right-2"
@tap.stop="() => toggleFavorite(scooter.id)"
>
<Heart
size="20"
:color="favoriteIds.includes(scooter.id) ? '#f97316' : '#d1d5db'"
:fill="favoriteIds.includes(scooter.id) ? '#f97316' : 'none'"
/>
</view>
<text class="font-medium text-sm block">{{ scooter.name }}</text>
<text class="text-xs text-gray-600 mt-1 block">
{{ scooter.year }} ·
<text v-if="scooter.batteryHealth">电池健康度{{ scooter.batteryHealth }}%</text>
<text v-if="scooter.mileage"> 行驶{{ scooter.mileage }}公里</text>
</text>
<view class="mt-2">
<text class="text-orange-500 font-bold">
¥{{ scooter.price.toLocaleString() }}
</text>
<text class="text-xs text-gray-500 mt-1 block">{{ scooter.school }}</text>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
......@@ -18,11 +197,135 @@ import Taro from '@tarojs/taro'
import '@tarojs/taro/html.css'
import { ref, onMounted } from 'vue'
import { useDidShow, useReady } from '@tarojs/taro'
import { Search, Clock, Star, Service, Right, Heart, Check } from '@nutui/icons-vue-taro'
import "./index.less";
const show = ref(false)
const onClick = () => {
show.value = true
// 响应式数据
const searchValue = ref('')
const favoriteIds = ref([])
// Banner图片
const bannerImages = ref([
'https://images.unsplash.com/photo-1558981806-ec527fa84c39?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60'
])
// 精品推荐数据
const featuredScooters = ref([
{
id: '1',
name: '小龟电动车',
year: '2023',
school: '上海理工大学',
price: 3880,
imageUrl: 'https://images.unsplash.com/photo-1558981285-6f0c94958bb6?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60',
isVerified: true
},
{
id: '2',
name: '立马电动车',
year: '2022',
school: '上海复旦大学',
price: 2999,
imageUrl: 'https://images.unsplash.com/photo-1558981403-c5f9899a28bc?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60',
isVerified: true
},
{
id: '3',
name: '雅迪电动车',
year: '2023',
school: '上海理工大学',
price: 4299,
imageUrl: 'https://images.unsplash.com/photo-1591637333184-19aa84b3e01f?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60'
},
{
id: '4',
name: '爱玛电动车',
year: '2022',
school: '上海复旦大学',
price: 3599,
imageUrl: 'https://images.unsplash.com/photo-1558980664-3a031cf67ea8?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60',
isVerified: true
}
])
// 最新上架数据
const latestScooters = ref([
{
id: '5',
name: '雅迪 豪华版',
year: '2023',
school: '上海理工大学',
price: 3200,
imageUrl: 'https://images.unsplash.com/photo-1558981403-c5f9899a28bc?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60',
isVerified: true,
batteryHealth: 98,
mileage: 1200
},
{
id: '6',
name: '台铃 战速',
year: '2022',
school: '上海理工大学',
price: 3800,
imageUrl: 'https://images.unsplash.com/photo-1558981285-6f0c94958bb6?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60',
batteryHealth: 92,
mileage: 2500
},
{
id: '7',
name: '小鸟电车',
year: '2023',
school: '上海复旦大学',
price: 3100,
imageUrl: 'https://images.unsplash.com/photo-1558980664-3a031cf67ea8?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60',
batteryHealth: 92,
mileage: 2000
},
{
id: '8',
name: '新日电动车',
year: '2024',
school: '上海同济大学',
price: 6700,
imageUrl: 'https://images.unsplash.com/photo-1595941069915-4ebc5197c14a?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60',
isVerified: true,
batteryHealth: 96,
mileage: 500
}
])
/**
* 切换收藏状态
* @param {string} scooterId - 电动车ID
*/
const toggleFavorite = (scooterId) => {
const index = favoriteIds.value.indexOf(scooterId)
if (index > -1) {
favoriteIds.value.splice(index, 1)
} else {
favoriteIds.value.push(scooterId)
}
}
/**
* 点击产品卡片
* @param {Object} scooter - 电动车信息
*/
const onProductClick = (scooter) => {
Taro.showToast({
title: `查看${scooter.name}`,
icon: 'none'
})
}
/**
* 点击认证车源
*/
const onCertifiedClick = () => {
Taro.showToast({
title: '查看认证车源',
icon: 'none'
})
}
// 生命周期钩子
......
export default {
navigationBarTitleText: '首页'
}
<template>
<view class="messages-page">
<!-- 顶部搜索栏 -->
<view class="search-container">
<view class="search-box">
<Search size="18" color="#9ca3af" />
<input
v-model="searchValue"
placeholder="搜索聊天记录..."
class="search-input"
/>
</view>
</view>
<!-- 消息列表 -->
<view class="messages-list">
<view
v-for="message in filteredMessages"
:key="message.id"
class="message-item"
@click="onMessageClick(message)"
>
<view class="avatar-container">
<image :src="message.avatar" class="avatar" mode="aspectFill" />
<view v-if="message.unreadCount > 0" class="unread-badge">
<text class="unread-count">{{ message.unreadCount > 99 ? '99+' : message.unreadCount }}</text>
</view>
</view>
<view class="message-content">
<view class="message-header">
<text class="sender-name">{{ message.senderName }}</text>
<text class="message-time">{{ formatTime(message.timestamp) }}</text>
</view>
<view class="message-preview">
<text class="preview-text" :class="{ 'unread': message.unreadCount > 0 }">
{{ message.lastMessage }}
</text>
<view v-if="message.type === 'image'" class="message-type-icon">
<Image size="16" color="#9ca3af" />
</view>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view v-if="filteredMessages.length === 0" class="empty-state">
<view class="empty-icon">
<Message size="48" color="#d1d5db" />
</view>
<text class="empty-title">暂无消息</text>
<text class="empty-subtitle">开始与买家或卖家聊天吧</text>
</view>
<!-- 浮动按钮 -->
<view class="floating-btn" @click="onNewMessage">
<Plus size="24" color="#ffffff" />
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
import { Search, Message, Plus } from '@nutui/icons-vue-taro'
import Taro from '@tarojs/taro'
// 响应式数据
const searchValue = ref('')
// 消息数据
const messages = ref([
{
id: 1,
senderName: '张同学',
avatar: 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=100&h=100&fit=crop&crop=face',
lastMessage: '这辆车还在吗?可以看看实物吗?',
timestamp: Date.now() - 300000, // 5分钟前
unreadCount: 2,
type: 'text'
},
{
id: 2,
senderName: '李小明',
avatar: 'https://images.unsplash.com/photo-1599566150163-29194dcaad36?w=100&h=100&fit=crop&crop=face',
lastMessage: '价格还能再便宜点吗?',
timestamp: Date.now() - 1800000, // 30分钟前
unreadCount: 0,
type: 'text'
},
{
id: 3,
senderName: '王美丽',
avatar: 'https://images.unsplash.com/photo-1494790108755-2616b612b786?w=100&h=100&fit=crop&crop=face',
lastMessage: '[图片]',
timestamp: Date.now() - 3600000, // 1小时前
unreadCount: 1,
type: 'image'
},
{
id: 4,
senderName: '陈大华',
avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=100&h=100&fit=crop&crop=face',
lastMessage: '好的,谢谢!',
timestamp: Date.now() - 7200000, // 2小时前
unreadCount: 0,
type: 'text'
},
{
id: 5,
senderName: '刘小红',
avatar: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=100&h=100&fit=crop&crop=face',
lastMessage: '车子的电池还好用吗?大概能跑多远?',
timestamp: Date.now() - 86400000, // 1天前
unreadCount: 0,
type: 'text'
},
{
id: 6,
senderName: '赵强',
avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=100&h=100&fit=crop&crop=face',
lastMessage: '明天下午有时间看车吗?',
timestamp: Date.now() - 172800000, // 2天前
unreadCount: 0,
type: 'text'
}
])
// 过滤后的消息列表
const filteredMessages = computed(() => {
if (!searchValue.value.trim()) {
return messages.value
}
return messages.value.filter(message =>
message.senderName.includes(searchValue.value) ||
message.lastMessage.includes(searchValue.value)
)
})
/**
* 格式化时间
* @param {number} timestamp - 时间戳
* @returns {string} 格式化后的时间
*/
const formatTime = (timestamp) => {
const now = Date.now()
const diff = now - timestamp
if (diff < 60000) { // 1分钟内
return '刚刚'
} else if (diff < 3600000) { // 1小时内
return `${Math.floor(diff / 60000)}分钟前`
} else if (diff < 86400000) { // 1天内
return `${Math.floor(diff / 3600000)}小时前`
} else if (diff < 604800000) { // 1周内
return `${Math.floor(diff / 86400000)}天前`
} else {
const date = new Date(timestamp)
return `${date.getMonth() + 1}/${date.getDate()}`
}
}
/**
* 消息点击事件
* @param {object} message - 消息对象
*/
const onMessageClick = (message) => {
// 清除未读数量
message.unreadCount = 0
// 跳转到聊天详情页面
Taro.navigateTo({
url: `/pages/chat/index?userId=${message.id}&userName=${message.senderName}`
})
}
/**
* 新建消息
*/
const onNewMessage = async () => {
try {
await Taro.showToast({
title: '新建消息',
icon: 'none'
})
} catch (error) {
console.error('新建消息失败:', error)
}
}
</script>
<style lang="less">
.messages-page {
min-height: 100vh;
background-color: #f9fafb;
padding-bottom: 100px;
}
.search-container {
padding: 16px;
background-color: #ffffff;
border-bottom: 1px solid #f3f4f6;
}
.search-box {
display: flex;
align-items: center;
background-color: #f9fafb;
border-radius: 24px;
padding: 12px 16px;
gap: 8px;
}
.search-input {
flex: 1;
border: none;
outline: none;
background: transparent;
font-size: 14px;
color: #374151;
}
.messages-list {
background-color: #ffffff;
}
.message-item {
display: flex;
align-items: center;
padding: 16px;
border-bottom: 1px solid #f3f4f6;
transition: background-color 0.2s;
}
.message-item:active {
background-color: #f9fafb;
}
.message-item:last-child {
border-bottom: none;
}
.avatar-container {
position: relative;
margin-right: 12px;
}
.avatar {
width: 48px;
height: 48px;
border-radius: 50%;
object-fit: cover;
}
.unread-badge {
position: absolute;
top: -4px;
right: -4px;
min-width: 20px;
height: 20px;
background-color: #ef4444;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
border: 2px solid #ffffff;
}
.unread-count {
font-size: 12px;
color: #ffffff;
font-weight: 500;
line-height: 1;
}
.message-content {
flex: 1;
min-width: 0;
}
.message-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
}
.sender-name {
font-size: 16px;
font-weight: 500;
color: #111827;
}
.message-time {
font-size: 12px;
color: #9ca3af;
}
.message-preview {
display: flex;
align-items: center;
gap: 4px;
}
.preview-text {
flex: 1;
font-size: 14px;
color: #6b7280;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.preview-text.unread {
color: #374151;
font-weight: 500;
}
.message-type-icon {
flex-shrink: 0;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px 20px;
text-align: center;
}
.empty-icon {
margin-bottom: 16px;
}
.empty-title {
font-size: 18px;
font-weight: 500;
color: #374151;
margin-bottom: 8px;
display: block;
}
.empty-subtitle {
font-size: 14px;
color: #9ca3af;
display: block;
}
.floating-btn {
position: fixed;
bottom: 100px;
right: 20px;
width: 56px;
height: 56px;
background-color: #f97316;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 12px rgba(249, 115, 22, 0.4);
transition: all 0.2s;
}
.floating-btn:active {
transform: scale(0.95);
box-shadow: 0 2px 8px rgba(249, 115, 22, 0.4);
}
</style>
\ No newline at end of file
/*
* @Date: 2025-07-01 17:55:04
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-07-01 18:04:14
* @FilePath: /jgdl/src/pages/post/index.config.js
* @Description: 文件描述
*/
export default {
navigationBarTitleText: '分类'
}
<template>
<view class="post-page">
<!-- 顶部搜索栏 -->
<view class="search-container">
<view class="search-box">
<Search size="18" color="#9ca3af" />
<input
v-model="searchValue"
placeholder="搜索电动车..."
class="search-input"
/>
</view>
</view>
<!-- 分类网格 -->
<view class="categories-grid">
<view
v-for="category in categories"
:key="category.id"
class="category-item"
@click="onCategoryClick(category)"
>
<view class="category-icon">
<component :is="category.icon" size="32" color="#f97316" />
</view>
<text class="category-name">{{ category.name }}</text>
<text class="category-count">{{ category.count }}辆</text>
</view>
</view>
<!-- 热门推荐 -->
<view class="section">
<view class="section-header">
<text class="section-title">热门推荐</text>
<view class="section-more" @click="onViewMore('hot')">
<text class="more-text">查看更多</text>
<Right size="16" color="#9ca3af" />
</view>
</view>
<view class="scooter-list">
<view
v-for="scooter in hotScooters"
:key="scooter.id"
class="scooter-card"
@click="onProductClick(scooter)"
>
<image :src="scooter.image" class="scooter-image" mode="aspectFill" />
<view class="scooter-info">
<text class="scooter-name">{{ scooter.name }}</text>
<text class="scooter-year">{{ scooter.year }}年</text>
<text class="scooter-school">{{ scooter.school }}</text>
<view class="scooter-footer">
<text class="scooter-price">¥{{ scooter.price }}</text>
<view class="favorite-btn" @click.stop="toggleFavorite(scooter.id)">
<Heart
size="20"
:color="favoriteIds.includes(scooter.id) ? '#ef4444' : '#9ca3af'"
:fill="favoriteIds.includes(scooter.id) ? '#ef4444' : 'none'"
/>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 最新发布 -->
<view class="section">
<view class="section-header">
<text class="section-title">最新发布</text>
<view class="section-more" @click="onViewMore('latest')">
<text class="more-text">查看更多</text>
<Right size="16" color="#9ca3af" />
</view>
</view>
<view class="scooter-list">
<view
v-for="scooter in latestScooters"
:key="scooter.id"
class="scooter-card"
@click="onProductClick(scooter)"
>
<image :src="scooter.image" class="scooter-image" mode="aspectFill" />
<view class="scooter-info">
<text class="scooter-name">{{ scooter.name }}</text>
<text class="scooter-year">{{ scooter.year }}年</text>
<text class="scooter-school">{{ scooter.school }}</text>
<view class="scooter-footer">
<text class="scooter-price">¥{{ scooter.price }}</text>
<view class="favorite-btn" @click.stop="toggleFavorite(scooter.id)">
<Heart
size="20"
:color="favoriteIds.includes(scooter.id) ? '#ef4444' : '#9ca3af'"
:fill="favoriteIds.includes(scooter.id) ? '#ef4444' : 'none'"
/>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { Search, Right, Cart, Star, Cart2, Category, Heart } from '@nutui/icons-vue-taro'
// 响应式数据
const searchValue = ref('')
const favoriteIds = ref([1, 3, 5])
// 分类数据
const categories = ref([
{ id: 1, name: '电动自行车', icon: Cart2, count: 128 },
{ id: 2, name: '电动摩托车', icon: Cart2, count: 86 },
{ id: 3, name: '电动汽车', icon: Star, count: 45 },
{ id: 4, name: '电动货车', icon: Cart, count: 23 },
{ id: 5, name: '平衡车', icon: Category, count: 67 },
{ id: 6, name: '滑板车', icon: Category, count: 92 }
])
// 热门推荐数据
const hotScooters = ref([
{
id: 1,
name: '小牛电动 NGT',
year: 2023,
school: '清华大学',
price: 3200,
image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=300&h=200&fit=crop'
},
{
id: 2,
name: '雅迪 DE2',
year: 2022,
school: '北京大学',
price: 2800,
image: 'https://images.unsplash.com/photo-1571068316344-75bc76f77890?w=300&h=200&fit=crop'
}
])
// 最新发布数据
const latestScooters = ref([
{
id: 3,
name: '爱玛 A500',
year: 2024,
school: '人民大学',
price: 2600,
image: 'https://images.unsplash.com/photo-1544191696-15693072e0d8?w=300&h=200&fit=crop'
},
{
id: 4,
name: '台铃 TDR',
year: 2023,
school: '北京理工',
price: 3500,
image: 'https://images.unsplash.com/photo-1558618047-3c8c76ca7d13?w=300&h=200&fit=crop'
}
])
/**
* 切换收藏状态
* @param {number} id - 电动车ID
*/
const toggleFavorite = (id) => {
const index = favoriteIds.value.indexOf(id)
if (index > -1) {
favoriteIds.value.splice(index, 1)
} else {
favoriteIds.value.push(id)
}
}
// 事件处理函数
const onCategoryClick = () => {
Taro.showToast({
title: '选择了分类',
icon: 'none'
})
}
const onProductClick = () => {
Taro.navigateTo({
url: '/pages/detail/index'
})
}
const onSearch = () => {
Taro.showToast({
title: '搜索功能',
icon: 'none'
})
}
/**
* 查看更多点击事件
* @param {string} type - 类型(hot/latest)
*/
const onViewMore = (type) => {
// 跳转到列表页面
}
</script>
<style lang="less">
.post-page {
min-height: 100vh;
background-color: #fef7ed;
padding-bottom: 100px;
}
.search-container {
padding: 16px;
background-color: #ffffff;
border-bottom: 1px solid #f3f4f6;
}
.search-box {
display: flex;
align-items: center;
background-color: #f9fafb;
border-radius: 24px;
padding: 12px 16px;
gap: 8px;
}
.search-input {
flex: 1;
border: none;
outline: none;
background: transparent;
font-size: 14px;
color: #374151;
}
.categories-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
padding: 20px 16px;
background-color: #ffffff;
margin-bottom: 12px;
}
.category-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 20px 12px;
background-color: #fef7ed;
border-radius: 12px;
border: 1px solid #fed7aa;
transition: all 0.2s;
}
.category-item:active {
transform: scale(0.95);
background-color: #fef3e2;
}
.category-icon {
margin-bottom: 8px;
}
.category-name {
font-size: 14px;
font-weight: 500;
color: #374151;
margin-bottom: 4px;
}
.category-count {
font-size: 12px;
color: #9ca3af;
}
.section {
background-color: #ffffff;
margin-bottom: 12px;
padding: 16px;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.section-title {
font-size: 18px;
font-weight: 600;
color: #111827;
}
.section-more {
display: flex;
align-items: center;
gap: 4px;
}
.more-text {
font-size: 14px;
color: #9ca3af;
}
.scooter-list {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.scooter-card {
background-color: #ffffff;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
transition: all 0.2s;
}
.scooter-card:active {
transform: scale(0.98);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.scooter-image {
width: 100%;
height: 120px;
object-fit: cover;
}
.scooter-info {
padding: 12px;
}
.scooter-name {
font-size: 14px;
font-weight: 600;
color: #111827;
margin-bottom: 4px;
display: block;
}
.scooter-year {
font-size: 12px;
color: #6b7280;
margin-bottom: 2px;
display: block;
}
.scooter-school {
font-size: 12px;
color: #9ca3af;
margin-bottom: 8px;
display: block;
}
.scooter-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.scooter-price {
font-size: 16px;
font-weight: 700;
color: #f97316;
}
.favorite-btn {
padding: 4px;
border-radius: 50%;
transition: all 0.2s;
}
.favorite-btn:active {
transform: scale(0.9);
}
</style>
export default {
navigationBarTitleText: '首页'
}
<template>
<view class="profile-page">
<!-- 用户信息卡片 -->
<view class="user-card">
<view class="user-info">
<image :src="userInfo.avatar" class="user-avatar" mode="aspectFill" />
<view class="user-details">
<text class="user-name">{{ userInfo.name }}</text>
<text class="user-school">{{ userInfo.school }}</text>
<view class="user-stats">
<text class="stat-item">信用分: {{ userInfo.creditScore }}</text>
<text class="stat-item">成交: {{ userInfo.dealCount }}笔</text>
</view>
</view>
</view>
<view class="edit-btn" @click="onEditProfile">
<Edit size="20" color="#f97316" />
</view>
</view>
<!-- 我的车辆 -->
<view class="section">
<view class="section-header">
<text class="section-title">我的车辆</text>
</view>
<view class="vehicle-stats">
<view class="stat-card" @click="onMyVehicles('selling')">
<text class="stat-number">{{ vehicleStats.selling }}</text>
<text class="stat-label">在售</text>
</view>
<view class="stat-card" @click="onMyVehicles('sold')">
<text class="stat-number">{{ vehicleStats.sold }}</text>
<text class="stat-label">已售</text>
</view>
<view class="stat-card" @click="onMyFavorites">
<text class="stat-number">{{ vehicleStats.favorites }}</text>
<text class="stat-label">收藏</text>
</view>
</view>
</view>
<!-- 功能菜单 -->
<view class="menu-section">
<view class="menu-item" @click="onOrderManagement">
<view class="menu-icon">
<Cart size="20" color="#f97316" />
</view>
<text class="menu-text">订单管理</text>
<Right size="16" color="#9ca3af" />
</view>
<view class="menu-item" @click="onCertification">
<view class="menu-icon">
<Service size="20" color="#f97316" />
</view>
<text class="menu-text">车辆认证</text>
<view class="certification-badge">
<text class="badge-text">{{ userInfo.isCertified ? '已认证' : '未认证' }}</text>
</view>
<Right size="16" color="#9ca3af" />
</view>
<view class="menu-item" @click="onWallet">
<view class="menu-icon">
<Shop size="20" color="#f97316" />
</view>
<text class="menu-text">我的钱包</text>
<text class="wallet-balance">¥{{ userInfo.balance }}</text>
<Right size="16" color="#9ca3af" />
</view>
<view class="menu-item" @click="onAddress">
<view class="menu-icon">
<Location size="20" color="#f97316" />
</view>
<text class="menu-text">收货地址</text>
<Right size="16" color="#9ca3af" />
</view>
</view>
<!-- 服务菜单 -->
<view class="menu-section">
<view class="menu-item" @click="onCustomerService">
<view class="menu-icon">
<Voice size="20" color="#f97316" />
</view>
<text class="menu-text">客服中心</text>
<Right size="16" color="#9ca3af" />
</view>
<view class="menu-item" @click="onFeedback">
<view class="menu-icon">
<Message size="20" color="#f97316" />
</view>
<text class="menu-text">意见反馈</text>
<Right size="16" color="#9ca3af" />
</view>
<view class="menu-item" @click="onAbout">
<view class="menu-icon">
<Tips size="20" color="#f97316" />
</view>
<text class="menu-text">关于我们</text>
<Right size="16" color="#9ca3af" />
</view>
</view>
<!-- 设置菜单 -->
<view class="menu-section">
<view class="menu-item" @click="onSettings">
<view class="menu-icon">
<Setting size="20" color="#f97316" />
</view>
<text class="menu-text">设置</text>
<Right size="16" color="#9ca3af" />
</view>
</view>
<!-- 退出登录 -->
<view class="logout-section">
<button class="logout-btn" @click="onLogout">退出登录</button>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import {
Edit, Right, Cart, Service, Shop, Location,
Voice, Message, Tips, Setting
} from '@nutui/icons-vue-taro'
import Taro from '@tarojs/taro'
// 用户信息
const userInfo = ref({
name: '张同学',
school: '清华大学',
avatar: 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=100&h=100&fit=crop&crop=face',
creditScore: 95,
dealCount: 12,
isCertified: true,
balance: 1580.50
})
// 车辆统计
const vehicleStats = ref({
selling: 3,
sold: 8,
favorites: 15
})
/**
* 编辑个人资料
*/
const onEditProfile = () => {
Taro.navigateTo({
url: '/pages/edit-profile/index'
})
}
/**
* 我的车辆
* @param {string} type - 车辆类型
*/
const onMyVehicles = (type) => {
Taro.navigateTo({
url: `/pages/my-vehicles/index?type=${type}`
})
}
/**
* 我的收藏
*/
const onMyFavorites = () => {
Taro.navigateTo({
url: '/pages/my-favorites/index'
})
}
/**
* 订单管理
*/
const onOrderManagement = () => {
Taro.navigateTo({
url: '/pages/order-management/index'
})
}
/**
* 车辆认证
*/
const onCertification = () => {
if (userInfo.value.isCertified) {
Taro.navigateTo({
url: '/pages/certification-status/index'
})
} else {
Taro.navigateTo({
url: '/pages/certification-apply/index'
})
}
}
/**
* 我的钱包
*/
const onWallet = () => {
Taro.navigateTo({
url: '/pages/wallet/index'
})
}
/**
* 收货地址
*/
const onAddress = () => {
Taro.navigateTo({
url: '/pages/address/index'
})
}
/**
* 客服中心
*/
const onCustomerService = () => {
Taro.showActionSheet({
itemList: ['在线客服', '电话客服', '常见问题'],
success: (res) => {
if (res.tapIndex === 0) {
// 在线客服
Taro.navigateTo({
url: '/pages/online-service/index'
})
} else if (res.tapIndex === 1) {
// 电话客服
Taro.makePhoneCall({
phoneNumber: '400-123-4567'
})
} else if (res.tapIndex === 2) {
// 常见问题
Taro.navigateTo({
url: '/pages/faq/index'
})
}
}
})
}
/**
* 意见反馈
*/
const onFeedback = () => {
Taro.navigateTo({
url: '/pages/feedback/index'
})
}
/**
* 关于我们
*/
const onAbout = () => {
Taro.navigateTo({
url: '/pages/about/index'
})
}
/**
* 设置
*/
const onSettings = () => {
Taro.navigateTo({
url: '/pages/settings/index'
})
}
/**
* 退出登录
*/
const onLogout = () => {
Taro.showModal({
title: '确认退出',
content: '确定要退出登录吗?',
success: (res) => {
if (res.confirm) {
// 清除用户数据
Taro.clearStorageSync()
// 跳转到登录页
Taro.redirectTo({
url: '/pages/auth/index'
})
Taro.showToast({
title: '已退出登录',
icon: 'success'
})
}
}
})
}
</script>
<style lang="less">
.profile-page {
min-height: 100vh;
background-color: #f9fafb;
padding-bottom: 100px;
}
.user-card {
background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
padding: 24px 16px;
display: flex;
justify-content: space-between;
align-items: center;
}
.user-info {
display: flex;
align-items: center;
flex: 1;
}
.user-avatar {
width: 64px;
height: 64px;
border-radius: 50%;
object-fit: cover;
border: 3px solid rgba(255, 255, 255, 0.3);
margin-right: 16px;
}
.user-details {
flex: 1;
}
.user-name {
font-size: 20px;
font-weight: 600;
color: #ffffff;
display: block;
margin-bottom: 4px;
}
.user-school {
font-size: 14px;
color: rgba(255, 255, 255, 0.8);
display: block;
margin-bottom: 8px;
}
.user-stats {
display: flex;
gap: 16px;
}
.stat-item {
font-size: 12px;
color: rgba(255, 255, 255, 0.9);
background-color: rgba(255, 255, 255, 0.2);
padding: 4px 8px;
border-radius: 12px;
}
.edit-btn {
width: 40px;
height: 40px;
background-color: rgba(255, 255, 255, 0.2);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.edit-btn:active {
transform: scale(0.95);
background-color: rgba(255, 255, 255, 0.3);
}
.section {
background-color: #ffffff;
margin: 12px 16px;
border-radius: 12px;
padding: 20px;
}
.section-header {
margin-bottom: 16px;
}
.section-title {
font-size: 18px;
font-weight: 600;
color: #111827;
display: block;
}
.vehicle-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
.stat-card {
text-align: center;
padding: 16px 8px;
background-color: #fef7ed;
border-radius: 8px;
border: 1px solid #fed7aa;
transition: all 0.2s;
}
.stat-card:active {
transform: scale(0.95);
background-color: #fef3e2;
}
.stat-number {
font-size: 24px;
font-weight: 700;
color: #f97316;
display: block;
margin-bottom: 4px;
}
.stat-label {
font-size: 14px;
color: #6b7280;
display: block;
}
.menu-section {
background-color: #ffffff;
margin: 12px 16px;
border-radius: 12px;
overflow: hidden;
}
.menu-item {
display: flex;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid #f3f4f6;
transition: background-color 0.2s;
}
.menu-item:last-child {
border-bottom: none;
}
.menu-item:active {
background-color: #f9fafb;
}
.menu-icon {
width: 40px;
height: 40px;
background-color: #fef7ed;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 12px;
}
.menu-text {
flex: 1;
font-size: 16px;
color: #374151;
}
.certification-badge {
margin-right: 8px;
}
.badge-text {
font-size: 12px;
color: #059669;
background-color: #d1fae5;
padding: 4px 8px;
border-radius: 12px;
}
.wallet-balance {
font-size: 16px;
font-weight: 600;
color: #f97316;
margin-right: 8px;
}
.logout-section {
margin: 24px 16px;
}
.logout-btn {
width: 100%;
padding: 16px;
background-color: #ffffff;
border: 1px solid #f87171;
border-radius: 12px;
color: #ef4444;
font-size: 16px;
font-weight: 500;
transition: all 0.2s;
}
.logout-btn:active {
background-color: #fef2f2;
transform: scale(0.98);
}
</style>
\ No newline at end of file
export default {
navigationBarTitleText: '首页'
}
<template>
<view class="sell-page">
<!-- 顶部标题 -->
<view class="header">
<text class="header-title">发布车辆</text>
<text class="header-subtitle">填写车辆信息,快速出售</text>
</view>
<!-- 表单内容 -->
<view class="form-container">
<!-- 车辆图片 -->
<view class="form-section">
<text class="section-title">车辆图片 <text class="required">*</text></text>
<view class="image-upload-container">
<view
v-for="(image, index) in vehicleImages"
:key="index"
class="image-item"
>
<image :src="image" class="uploaded-image" mode="aspectFill" />
<view class="image-delete" @click="removeImage(index)">
<Close size="16" color="#ffffff" />
</view>
</view>
<view
v-if="vehicleImages.length < 6"
class="image-upload-btn"
@click="uploadImage"
>
<Plus size="24" color="#9ca3af" />
<text class="upload-text">添加图片</text>
</view>
</view>
<text class="form-tip">最多上传6张图片,第一张为封面图</text>
</view>
<!-- 基本信息 -->
<view class="form-section">
<text class="section-title">基本信息</text>
<view class="form-item">
<text class="form-label">车辆名称 <text class="required">*</text></text>
<input
v-model="formData.name"
placeholder="请输入车辆名称"
class="form-input"
/>
</view>
<view class="form-item">
<text class="form-label">车辆品牌 <text class="required">*</text></text>
<picker
:range="brands"
:value="brandIndex"
@change="onBrandChange"
class="form-picker"
>
<view class="picker-content">
<text class="picker-text">{{ brands[brandIndex] || '请选择品牌' }}</text>
<Right size="16" color="#9ca3af" />
</view>
</picker>
</view>
<view class="form-item">
<text class="form-label">购买年份 <text class="required">*</text></text>
<picker
:range="years"
:value="yearIndex"
@change="onYearChange"
class="form-picker"
>
<view class="picker-content">
<text class="picker-text">{{ years[yearIndex] || '请选择年份' }}</text>
<Right size="16" color="#9ca3af" />
</view>
</picker>
</view>
<view class="form-item">
<text class="form-label">车辆类型 <text class="required">*</text></text>
<picker
:range="types"
:value="typeIndex"
@change="onTypeChange"
class="form-picker"
>
<view class="picker-content">
<text class="picker-text">{{ types[typeIndex] || '请选择类型' }}</text>
<Right size="16" color="#9ca3af" />
</view>
</picker>
</view>
<view class="form-item">
<text class="form-label">出售价格 <text class="required">*</text></text>
<view class="price-input-container">
<text class="price-symbol">¥</text>
<input
v-model="formData.price"
placeholder="请输入价格"
type="number"
class="price-input"
/>
</view>
</view>
</view>
<!-- 车辆状况 -->
<view class="form-section">
<text class="section-title">车辆状况</text>
<view class="form-item">
<text class="form-label">使用时长</text>
<picker
:range="usagePeriods"
:value="usageIndex"
@change="onUsageChange"
class="form-picker"
>
<view class="picker-content">
<text class="picker-text">{{ usagePeriods[usageIndex] || '请选择使用时长' }}</text>
<Right size="16" color="#9ca3af" />
</view>
</picker>
</view>
<view class="form-item">
<text class="form-label">车辆描述</text>
<textarea
v-model="formData.description"
placeholder="请描述车辆的具体情况,如外观、性能、配件等"
class="form-textarea"
maxlength="500"
/>
<text class="char-count">{{ formData.description.length }}/500</text>
</view>
</view>
<!-- 联系方式 -->
<view class="form-section">
<text class="section-title">联系方式</text>
<view class="form-item">
<text class="form-label">手机号码 <text class="required">*</text></text>
<input
v-model="formData.phone"
placeholder="请输入手机号码"
type="number"
class="form-input"
/>
</view>
<view class="form-item">
<text class="form-label">所在学校 <text class="required">*</text></text>
<input
v-model="formData.school"
placeholder="请输入所在学校"
class="form-input"
/>
</view>
<view class="form-item">
<text class="form-label">交易地点</text>
<input
v-model="formData.location"
placeholder="请输入具体交易地点"
class="form-input"
/>
</view>
</view>
</view>
<!-- 底部按钮 -->
<view class="bottom-actions">
<button class="preview-btn" @click="onPreview">预览</button>
<button class="publish-btn" @click="onPublish">发布车辆</button>
</view>
</view>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { Close, Plus, Right } from '@nutui/icons-vue-taro'
import Taro from '@tarojs/taro'
// 响应式数据
const vehicleImages = ref([])
const brandIndex = ref(-1)
const yearIndex = ref(-1)
const typeIndex = ref(-1)
const usageIndex = ref(-1)
// 表单数据
const formData = reactive({
name: '',
brand: '',
year: '',
type: '',
price: '',
usage: '',
description: '',
phone: '',
school: '',
location: ''
})
// 选择器数据
const brands = ['小牛电动', '雅迪', '爱玛', '台铃', '绿源', '新日', '立马', '其他']
const years = ['2024年', '2023年', '2022年', '2021年', '2020年', '2019年', '2018年', '更早']
const types = ['电动自行车', '电动摩托车', '电动汽车', '平衡车', '滑板车', '其他']
const usagePeriods = ['3个月以内', '3-6个月', '6个月-1年', '1-2年', '2-3年', '3年以上']
/**
* 上传图片
*/
const uploadImage = () => {
Taro.chooseImage({
count: 6 - vehicleImages.value.length,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
vehicleImages.value.push(...res.tempFilePaths)
}
})
}
/**
* 删除图片
* @param {number} index - 图片索引
*/
const removeImage = (index) => {
vehicleImages.value.splice(index, 1)
}
/**
* 品牌选择事件
* @param {object} e - 事件对象
*/
const onBrandChange = (e) => {
brandIndex.value = e.detail.value
formData.brand = brands[e.detail.value]
}
/**
* 年份选择事件
* @param {object} e - 事件对象
*/
const onYearChange = (e) => {
yearIndex.value = e.detail.value
formData.year = years[e.detail.value]
}
/**
* 类型选择事件
* @param {object} e - 事件对象
*/
const onTypeChange = (e) => {
typeIndex.value = e.detail.value
formData.type = types[e.detail.value]
}
/**
* 使用时长选择事件
* @param {object} e - 事件对象
*/
const onUsageChange = (e) => {
usageIndex.value = e.detail.value
formData.usage = usagePeriods[e.detail.value]
}
/**
* 预览功能
*/
const onPreview = () => {
if (!validateForm()) return
Taro.showToast({
title: '预览功能开发中',
icon: 'none'
})
}
/**
* 发布车辆
*/
const onPublish = () => {
if (!validateForm()) return
Taro.showLoading({ title: '发布中...' })
// 模拟发布请求
setTimeout(() => {
Taro.hideLoading()
Taro.showToast({
title: '发布成功',
icon: 'success'
})
// 发布成功后跳转到首页
setTimeout(() => {
Taro.switchTab({ url: '/pages/index/index' })
}, 1500)
}, 2000)
}
/**
* 表单验证
* @returns {boolean} 验证结果
*/
const validateForm = () => {
if (vehicleImages.value.length === 0) {
Taro.showToast({ title: '请上传车辆图片', icon: 'none' })
return false
}
if (!formData.name.trim()) {
Taro.showToast({ title: '请输入车辆名称', icon: 'none' })
return false
}
if (!formData.brand) {
Taro.showToast({ title: '请选择车辆品牌', icon: 'none' })
return false
}
if (!formData.year) {
Taro.showToast({ title: '请选择购买年份', icon: 'none' })
return false
}
if (!formData.type) {
Taro.showToast({ title: '请选择车辆类型', icon: 'none' })
return false
}
if (!formData.price || formData.price <= 0) {
Taro.showToast({ title: '请输入正确的价格', icon: 'none' })
return false
}
if (!formData.phone.trim()) {
Taro.showToast({ title: '请输入手机号码', icon: 'none' })
return false
}
if (!/^1[3-9]\d{9}$/.test(formData.phone)) {
Taro.showToast({ title: '请输入正确的手机号码', icon: 'none' })
return false
}
if (!formData.school.trim()) {
Taro.showToast({ title: '请输入所在学校', icon: 'none' })
return false
}
return true
}
</script>
<style lang="less">
.sell-page {
min-height: 100vh;
background-color: #f9fafb;
padding-bottom: 80px;
}
.header {
background-color: #ffffff;
padding: 20px 16px;
border-bottom: 1px solid #f3f4f6;
}
.header-title {
font-size: 24px;
font-weight: 700;
color: #111827;
display: block;
margin-bottom: 4px;
}
.header-subtitle {
font-size: 14px;
color: #6b7280;
display: block;
}
.form-container {
padding: 0 16px;
}
.form-section {
background-color: #ffffff;
border-radius: 12px;
padding: 20px;
margin: 12px 0;
}
.section-title {
font-size: 18px;
font-weight: 600;
color: #111827;
margin-bottom: 16px;
display: block;
}
.required {
color: #ef4444;
}
.image-upload-container {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin-bottom: 8px;
}
.image-item {
position: relative;
aspect-ratio: 1;
border-radius: 8px;
overflow: hidden;
}
.uploaded-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.image-delete {
position: absolute;
top: 4px;
right: 4px;
width: 24px;
height: 24px;
background-color: rgba(0, 0, 0, 0.6);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.image-upload-btn {
aspect-ratio: 1;
border: 2px dashed #d1d5db;
border-radius: 8px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
background-color: #f9fafb;
}
.upload-text {
font-size: 12px;
color: #9ca3af;
}
.form-tip {
font-size: 12px;
color: #9ca3af;
display: block;
}
.form-item {
margin-bottom: 16px;
}
.form-label {
font-size: 14px;
font-weight: 500;
color: #374151;
margin-bottom: 8px;
display: block;
}
.form-input {
width: 100%;
padding: 12px 16px;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 14px;
color: #374151;
background-color: #ffffff;
}
.form-input:focus {
border-color: #f97316;
outline: none;
}
.form-picker {
width: 100%;
}
.picker-content {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
border: 1px solid #d1d5db;
border-radius: 8px;
background-color: #ffffff;
}
.picker-text {
font-size: 14px;
color: #374151;
}
.price-input-container {
display: flex;
align-items: center;
border: 1px solid #d1d5db;
border-radius: 8px;
background-color: #ffffff;
}
.price-symbol {
padding: 12px 0 12px 16px;
font-size: 14px;
color: #374151;
font-weight: 500;
}
.price-input {
flex: 1;
padding: 12px 16px 12px 4px;
border: none;
font-size: 14px;
color: #374151;
background: transparent;
}
.form-textarea {
width: 100%;
min-height: 80px;
padding: 12px 16px;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 14px;
color: #374151;
background-color: #ffffff;
resize: none;
}
.form-textarea:focus {
border-color: #f97316;
outline: none;
}
.char-count {
font-size: 12px;
color: #9ca3af;
text-align: right;
margin-top: 4px;
display: block;
}
.bottom-actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #ffffff;
padding: 12px 16px;
border-top: 1px solid #f3f4f6;
display: flex;
gap: 12px;
}
.preview-btn {
flex: 1;
padding: 12px;
border: 1px solid #f97316;
border-radius: 8px;
background-color: #ffffff;
color: #f97316;
font-size: 16px;
font-weight: 500;
}
.publish-btn {
flex: 2;
padding: 12px;
border: none;
border-radius: 8px;
background-color: #f97316;
color: #ffffff;
font-size: 16px;
font-weight: 500;
}
.preview-btn:active {
background-color: #fef7ed;
}
.publish-btn:active {
background-color: #ea580c;
}
</style>
\ No newline at end of file