hookehuyr

feat(页面): 重构发布页并添加NutUI组件

- 使用NutUI组件重构发布页界面,包括搜索栏、菜单筛选和按钮
- 替换Right图标为RectRight以保持一致性
- 添加加载更多功能和模拟数据生成逻辑
- 更新车辆卡片布局和样式,增加认证标识
......@@ -8,6 +8,10 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
NavBar: typeof import('./src/components/navBar.vue')['default']
NutButton: typeof import('@nutui/nutui-taro')['Button']
NutLoading: typeof import('@nutui/nutui-taro')['Loading']
NutMenu: typeof import('@nutui/nutui-taro')['Menu']
NutMenuItem: typeof import('@nutui/nutui-taro')['MenuItem']
NutSearchbar: typeof import('@nutui/nutui-taro')['Searchbar']
NutSwiper: typeof import('@nutui/nutui-taro')['Swiper']
NutSwiperItem: typeof import('@nutui/nutui-taro')['SwiperItem']
......
<!--
* @Date: 2025-06-28 10:33:00
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-07-01 21:50:54
* @LastEditTime: 2025-07-01 22:57:26
* @FilePath: /jgdl/src/pages/index/index.vue
* @Description: 捡个电驴首页
-->
......@@ -59,7 +59,7 @@
<text class="text-lg font-medium">精品推荐</text>
<view class="text-sm text-gray-500 flex items-center">
<text>更多</text>
<Right size="16" />
<RectRight size="12" />
</view>
</view>
<view class="grid grid-cols-2 gap-3">
......@@ -105,7 +105,7 @@
<text class="text-lg font-medium">最新上架</text>
<view class="text-sm text-gray-500 flex items-center">
<text>更多</text>
<Right size="16" />
<RectRight size="12" />
</view>
</view>
<view class="flex flex-col">
......@@ -143,7 +143,7 @@
</view>
</view>
</view>
<!-- 自定义TabBar -->
<TabBar />
</view>
......@@ -154,7 +154,7 @@ import Taro from '@tarojs/taro'
// import '@tarojs/taro/html.css' 和 nutui组件居然有冲突?
import { ref, onMounted } from 'vue'
import { useDidShow, useReady } from '@tarojs/taro'
import { Clock, Star, Right, Addfollow, HeartFill, Check, Search2, Shop } from '@nutui/icons-vue-taro'
import { Clock, Star, RectRight, Addfollow, HeartFill, Check, Search2, Shop } from '@nutui/icons-vue-taro'
import TabBar from '@/components/TabBar.vue'
import "./index.less";
......
<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 class="flex flex-col bg-white 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 -->
<nut-searchbar v-model="searchValue" placeholder="搜索品牌型号" shape="round" background="transparent"
input-background="#ffffff">
<template #leftin>
<Search2 />
</template>
</nut-searchbar>
</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'"
/>
<!-- Filter options -->
<nut-menu>
<nut-menu-item v-model="selectedBrand" :options="brandOptions" @change="onBrandChange" />
<nut-menu-item v-model="selectedYear" :options="yearOptions" @change="onYearChange" />
<nut-menu-item v-model="selectedSchool" :options="schoolOptions" @change="onSchoolChange" />
</nut-menu>
<!-- Scooter listings - 参考PostPage.tsx -->
<view class="flex-1 p-4 vehicle-list">
<view class="space-y-4">
<view v-for="scooter in scooters" :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)">
<Addfollow v-if="!favoriteIds.includes(scooter.id)" size="16" color="#9ca3af" />
<HeartFill v-else size="16" color="#ef4444" />
</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>
<!-- 最新发布 -->
<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 class="load-more-container">
<view v-if="loading" class="loading-container">
<view class="loading-spinner"></view>
<text class="loading-text">加载中...</text>
</view>
<view v-else-if="noMoreData" class="no-more-data">
<text>没有更多数据了</text>
</view>
<nut-button v-else shape="round" type="info" @click="loadMoreData">点击加载更多</nut-button>
</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'"
/>
<!-- Featured recommendations section -->
<view class="mt-6 mb-20 ml-4 mr-4">
<view class="flex justify-between items-center mb-3">
<text class="text-lg font-medium">精品推荐</text>
<view class="text-sm text-gray-500 flex items-center" @tap="onViewMore">
<text>更多</text>
<RectRight size="12" />
</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)">
<Addfollow v-if="!favoriteIds.includes(scooter.id)" size="20" color="#ffffff" />
<HeartFill v-else size="20" color="#ef4444" />
</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 class="text-xs text-gray-500 mt-1 block">{{ scooter.school }}</text>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 自定义TabBar -->
<TabBar />
</view>
<!-- 自定义TabBar -->
<TabBar />
</template>
<script setup>
import { ref } from 'vue'
import Taro from '@tarojs/taro'
import { Search, Right, Cart, Star, Cart2, Category, Heart } from '@nutui/icons-vue-taro'
import { Search2, Right, RectRight, Check, Addfollow, HeartFill } from '@nutui/icons-vue-taro'
import TabBar from '@/components/TabBar.vue'
// 响应式数据
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 favoriteIds = ref(['5', '7', '1'])
// 无限滚动相关状态
const loading = ref(false)
const noMoreData = ref(false)
const currentPage = ref(1)
const pageSize = ref(4)
// Filter states - 使用NutUI Menu组件
const selectedBrand = ref('全部品牌')
const selectedYear = ref('出厂年份')
const selectedSchool = ref('所在学校')
// Menu选项数据
const brandOptions = ref([
{ text: '全部品牌', value: '全部品牌' },
{ text: '雅迪', value: '雅迪' },
{ text: '台铃', value: '台铃' },
{ text: '小鸟', value: '小鸟' },
{ text: '新日', value: '新日' },
{ text: '爱玛', value: '爱玛' },
{ text: '小牛', value: '小牛' }
])
const yearOptions = ref([
{ text: '出厂年份', value: '出厂年份' },
{ text: '2024年', value: '2024年' },
{ text: '2023年', value: '2023年' },
{ text: '2022年', value: '2022年' },
{ text: '2021年', value: '2021年' },
{ text: '2020年', value: '2020年' }
])
const schoolOptions = ref([
{ text: '所在学校', value: '所在学校' },
{ text: '上海理工大学', value: '上海理工大学' },
{ text: '上海复旦大学', value: '上海复旦大学' },
{ text: '上海同济大学', value: '上海同济大学' },
{ text: '上海交通大学', value: '上海交通大学' }
])
// 热门推荐数据
const hotScooters = ref([
// 主要车辆列表数据 - 参考PostPage.tsx
const scooters = ref([
{
id: 1,
name: '小牛电动 NGT',
year: 2023,
school: '清华大学',
id: '5',
name: '雅迪 豪华版',
year: '2023年',
school: '上海理工大学',
price: 3200,
image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=300&h=200&fit=crop'
imageUrl: 'https://images.unsplash.com/photo-1567922045116-2a00fae2ed03?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60',
batteryHealth: 98,
mileage: 1200,
brand: '雅迪',
isVerified: true
},
{
id: '6',
name: '台铃 战速',
year: '2022年',
school: '上海理工大学',
price: 3800,
imageUrl: 'https://images.unsplash.com/photo-1567922045116-2a00fae2ed03?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60',
batteryHealth: 92,
mileage: 2500,
brand: '台铃'
},
{
id: 2,
name: '雅迪 DE2',
year: 2022,
school: '北京大学',
price: 2800,
image: 'https://images.unsplash.com/photo-1571068316344-75bc76f77890?w=300&h=200&fit=crop'
id: '7',
name: '小鸟电车',
year: '2023年',
school: '上海复旦大学',
price: 3100,
imageUrl: 'https://images.unsplash.com/photo-1567922045116-2a00fae2ed03?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60',
batteryHealth: 92,
mileage: 2000,
brand: '小鸟'
},
{
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',
batteryHealth: 96,
mileage: 500,
brand: '新日',
isVerified: true
}
])
// 最新发布数据
const latestScooters = ref([
// 精品推荐数据 - 参考PostPage.tsx
const featuredScooters = 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: '1',
name: '小龟电动车',
year: '2023年',
school: '上海理工大学',
price: 3880,
batteryHealth: 92,
mileage: 2000,
imageUrl: 'https://images.unsplash.com/photo-1558981285-6f0c94958bb6?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60',
isVerified: true
},
{
id: 4,
name: '台铃 TDR',
year: 2023,
school: '北京理工',
price: 3500,
image: 'https://images.unsplash.com/photo-1558618047-3c8c76ca7d13?w=300&h=200&fit=crop'
id: '2',
name: '立马电动车',
year: '2022年',
school: '上海复旦大学',
price: 2999,
batteryHealth: 95,
mileage: 1500,
imageUrl: 'https://images.unsplash.com/photo-1558981403-c5f9899a28bc?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60'
}
])
/**
* 切换收藏状态
* @param {number} id - 电动车ID
* @param {string} scooterId - 电动车ID
*/
const toggleFavorite = (id) => {
const index = favoriteIds.value.indexOf(id)
const toggleFavorite = (scooterId) => {
const index = favoriteIds.value.indexOf(scooterId)
if (index > -1) {
favoriteIds.value.splice(index, 1)
} else {
favoriteIds.value.push(id)
favoriteIds.value.push(scooterId)
}
}
// 事件处理函数
const onCategoryClick = () => {
/**
* 点击产品卡片
* @param {Object} scooter - 电动车信息
*/
const onProductClick = (scooter) => {
Taro.showToast({
title: '选择了分类',
title: `查看${scooter.name}`,
icon: 'none'
})
}
const onProductClick = () => {
Taro.navigateTo({
url: '/pages/detail/index'
})
}
/**
* 查看更多点击事件
*/
const onViewMore = () => {
// 跳转到列表页面
}
</script>
<style lang="less">
.post-page {
min-height: 100vh;
background-color: #fef7ed;
padding-bottom: 100px;
Taro.showToast({
title: '查看更多精品推荐',
icon: 'none'
})
}
.search-container {
padding: 16px;
background-color: #ffffff;
border-bottom: 1px solid #f3f4f6;
// Menu组件事件处理方法
/**
* 品牌选择变化事件
* @param {string} value - 选中的品牌值
*/
const onBrandChange = (value) => {
selectedBrand.value = value
// 这里可以添加过滤逻辑
}
.search-box {
display: flex;
align-items: center;
background-color: #f9fafb;
border-radius: 24px;
padding: 12px 16px;
gap: 8px;
/**
* 年份选择变化事件
* @param {string} value - 选中的年份值
*/
const onYearChange = (value) => {
selectedYear.value = value
// 这里可以添加过滤逻辑
}
.search-input {
flex: 1;
border: none;
outline: none;
background: transparent;
font-size: 14px;
color: #374151;
/**
* 学校选择变化事件
* @param {string} value - 选中的学校值
*/
const onSchoolChange = (value) => {
selectedSchool.value = value
// 这里可以添加过滤逻辑
}
.categories-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
padding: 20px 16px;
background-color: #ffffff;
margin-bottom: 12px;
/**
* 生成模拟车辆数据
* @param {number} page - 页码
* @param {number} size - 每页数量
* @returns {Array} 车辆数据数组
*/
const generateMockData = (page, size) => {
const brands = ['雅迪', '台铃', '小鸟', '新日', '爱玛', '小牛', '绿源', '立马']
const schools = ['上海理工大学', '上海复旦大学', '上海同济大学', '上海交通大学', '华东师范大学', '上海大学']
const years = ['2024年', '2023年', '2022年', '2021年', '2020年']
const images = [
'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',
'https://images.unsplash.com/photo-1583568671741-c70dafa8e8e7?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60',
'https://images.unsplash.com/photo-1595941069915-4ebc5197c14a?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60',
'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'
]
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 school = schools[Math.floor(Math.random() * schools.length)]
const year = years[Math.floor(Math.random() * years.length)]
const image = images[Math.floor(Math.random() * images.length)]
data.push({
id: `mock_${index + 100}`,
name: `${brand} ${['豪华版', '标准版', '运动版', '经典版'][Math.floor(Math.random() * 4)]}`,
year: year,
school: school,
price: Math.floor(Math.random() * 5000) + 2000,
imageUrl: image,
batteryHealth: Math.floor(Math.random() * 20) + 80,
mileage: Math.floor(Math.random() * 3000) + 500,
brand: brand,
isVerified: Math.random() > 0.7
})
}
return data
}
.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;
/**
* 模拟异步请求加载更多数据
*/
const loadMoreData = async () => {
if (loading.value || noMoreData.value) return
loading.value = true
try {
// 模拟网络请求延迟
await new Promise(resolve => setTimeout(resolve, 1000 + Math.random() * 1000))
// 模拟最多加载5页数据
if (currentPage.value >= 5) {
noMoreData.value = true
loading.value = false
return
}
currentPage.value++
const newData = generateMockData(currentPage.value, pageSize.value)
scooters.value.push(...newData)
} catch (error) {
Taro.showToast({
title: '加载失败,请重试',
icon: 'none'
})
} finally {
loading.value = false
}
}
</script>
.category-item:active {
transform: scale(0.95);
background-color: #fef3e2;
<style lang="less">
// 使用Tailwind CSS类,只保留必要的自定义样式
.space-y-4>view:not(:first-child) {
margin-top: 1rem;
}
.category-icon {
margin-bottom: 8px;
.grid {
display: grid;
}
.category-name {
font-size: 14px;
font-weight: 500;
color: #374151;
margin-bottom: 4px;
.grid-cols-2 {
grid-template-columns: repeat(2, 1fr);
}
.category-count {
font-size: 12px;
color: #9ca3af;
.gap-3 {
gap: 0.75rem;
}
.section {
background-color: #ffffff;
margin-bottom: 12px;
padding: 16px;
// 确保图片正确显示
image {
display: block;
}
.section-header {
// 加载动画
.load-more-container {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.section-title {
font-size: 18px;
font-weight: 600;
color: #111827;
justify-content: center;
padding: 2rem 0;
}
.section-more {
.loading-container {
display: flex;
align-items: center;
gap: 4px;
}
.more-text {
font-size: 14px;
color: #9ca3af;
color: #666;
}
.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);
.loading-spinner {
width: 20px;
height: 20px;
border: 2px solid #f3f3f3;
border-top: 2px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-right: 8px;
}
.scooter-image {
width: 100%;
height: 120px;
object-fit: cover;
.loading-text {
font-size: 0.85rem;
}
.scooter-info {
padding: 12px;
.no-more-data {
color: #999;
font-size: 0.85rem;
}
.scooter-name {
.load-more-btn {
background-color: #007aff;
color: white;
border: none;
border-radius: 20px;
padding: 10px 20px;
font-size: 14px;
font-weight: 600;
color: #111827;
margin-bottom: 4px;
display: block;
cursor: pointer;
}
.scooter-year {
font-size: 12px;
color: #6b7280;
margin-bottom: 2px;
display: block;
.load-more-btn:hover {
background-color: #0056cc;
}
.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;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
.favorite-btn:active {
transform: scale(0.9);
100% {
transform: rotate(360deg);
}
}
</style>
......