index.vue 13.5 KB
<!--
 * @Date: 2022-09-19 14:11:06
 * @LastEditors: hookehuyr hookehuyr@gmail.com
 * @LastEditTime: 2025-07-04 16:57:31
 * @FilePath: /jgdl/src/pages/search/index.vue
 * @Description: 搜索页面
-->
<template>
    <view class="search-page">
        <view class="flex flex-col bg-white min-h-screen">
            <!-- Header -->
            <nut-sticky>
                <view class="bg-orange-400 p-4 pt-4 pb-4">
                    <nut-row type="flex" justify="center" align="center">
                        <nut-col span="3">
                            <view class="text-xl font-bold text-white">搜索</view>
                        </nut-col>
                        <nut-col span="21">
                            <!-- Search Bar -->
                            <nut-searchbar v-model="searchValue" placeholder="搜索商品名称、品牌、型号" @search="onSearch" @blur="onBlurSearch" @clear="onClearSearch" shape="round" background="transparent" input-background="#ffffff">
                                <template #leftin>
                                    <Search2 />
                                </template>
                            </nut-searchbar>
                        </nut-col>
                    </nut-row>
                </view>

                <!-- 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>
            </nut-sticky>

            <!-- Search Results -->
            <view class="flex-1">
                <!-- 滚动列表 -->
                <scroll-view
                    class="search-results-list"
                    :style="scrollStyle"
                    :scroll-y="true"
                    @scrolltolower="loadMore"
                    :lower-threshold="50"
                    :enable-flex="false"
                >
                    <view class="p-4">
                        <!-- 搜索结果统计 -->
                        <view v-if="searchResults.length > 0" class="mb-4">
                            <text class="text-sm text-gray-600">找到 {{ totalCount }} 个相关结果</text>
                        </view>

                        <!-- 搜索结果网格布局 -->
                        <view class="grid grid-cols-2 gap-3">
                            <view v-for="scooter in searchResults" :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-3 w-7 h-7 rounded-full bg-white bg-opacity-90" @tap.stop="() => toggleFavorite(scooter.id)" style="padding-top: 12rpx; padding-left: 10rpx;">
                                        <Heart1 v-if="!favoriteIds.includes(scooter.id)" size="22" :color="'#9ca3af'" />
                                        <HeartFill v-else size="22" :color="'#ef4444'" />
                                    </view>
                                    <view v-if="scooter.isVerified"
                                        class="absolute bottom-4 right-4 text-white text-xs px-1.5 py-0.5 rounded flex items-center" style="background-color: #EB5305;">
                                        <Check size="12" color="#ffffff" class="mr-0.5" />
                                        <text class="text-white">认证</text>
                                    </view>
                                </view>
                                <view class="p-2 pl-3">
                                    <text class="font-medium text-sm block">{{ scooter.name }}</text>
                                    <text class="text-xs text-gray-500 block mt-1 mb-1">
                                        {{ scooter.year }} · {{ scooter.school }}
                                        <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" style="font-size: 1.25rem;">
                                            ¥{{ scooter.price.toLocaleString() }}
                                        </text>
                                    </view>
                                </view>
                            </view>
                        </view>

                        <!-- 初始空状态 -->
                        <view v-if="searchResults.length === 0 && !loading && !hasSearched" class="empty-state text-center py-20">
                            <view class="empty-icon mb-4">
                                <Search2 size="80" color="#d1d5db" />
                            </view>
                            <text class="text-lg font-medium text-gray-600 block mb-2">搜索电动车</text>
                            <text class="text-sm text-gray-400 block">输入品牌型号,找到心仪的电动车</text>
                        </view>

                        <!-- 搜索无结果状态 -->
                        <view v-if="searchResults.length === 0 && !loading && hasSearched" class="no-results-state text-center py-20">
                            <view class="no-results-icon mb-4">
                                <Search2 size="60" color="#9ca3af" />
                            </view>
                            <text class="text-base font-medium text-gray-600 block mb-2">暂无搜索结果</text>
                            <text class="text-sm text-gray-400 block">试试其他关键词或调整筛选条件</text>
                        </view>

                        <!-- 加载更多 -->
                        <view v-if="searchResults.length > 0" class="load-more-container mt-6">
                            <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-center">
                                <text class="text-gray-500">没有更多数据了</text>
                            </view>
                        </view>
                    </view>
                </scroll-view>
            </view>
        </view>
    </view>
</template>

<script setup>
import { ref, computed, onMounted } from 'vue'
import Taro from '@tarojs/taro'
import { Search2, Check, Heart1, HeartFill } from '@nutui/icons-vue-taro'
import "./index.less";

// 响应式数据
const searchValue = ref('')
const favoriteIds = ref(['5', '7', '1'])
const hasSearched = ref(false)

// 滚动相关
const loading = ref(false)
const noMoreData = ref(false)
const currentPage = ref(1)
const pageSize = ref(10)
const totalCount = ref(0)

// Filter states
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 searchResults = ref([])

// 滚动样式
const scrollStyle = computed(() => {
    return {
        height: 'calc(100vh - 200rpx)'
    }
})

/**
 * 生成模拟数据
 * @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-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-1595941069915-4ebc5197c14a?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60'
    ]

    const data = []
    const startId = (page - 1) * size + 1

    for (let i = 0; i < 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: `search_${startId + i}`,
            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() * 30) + 70,
            mileage: Math.floor(Math.random() * 5000) + 500,
            brand: brand,
            isVerified: Math.random() > 0.6
        })
    }

    return data
}

/**
 * 执行搜索
 */
const performSearch = () => {
    if (!searchValue.value.trim()) {
        clearSearchResults()
        return
    }

    loading.value = true
    hasSearched.value = true
    currentPage.value = 1
    noMoreData.value = false

    // 模拟API调用延迟
    setTimeout(() => {
        const mockData = generateMockData(1, pageSize.value)
        searchResults.value = mockData
        totalCount.value = 50 // 模拟总数
        loading.value = false
    }, 500)
}

/**
 * 清除搜索结果
 */
const clearSearchResults = () => {
    searchResults.value = []
    hasSearched.value = false
    currentPage.value = 1
    noMoreData.value = false
    totalCount.value = 0
}

/**
 * 搜索事件处理
 */
const onSearch = () => {
    performSearch()
}

/**
 * 搜索框失焦事件
 */
const onBlurSearch = () => {
    if (searchValue.value.trim()) {
        performSearch()
    } else {
        // 如果搜索框为空,清除搜索结果
        clearSearchResults()
    }
}

const onClearSearch = () => {
    clearSearchResults()
}

/**
 * 品牌筛选变化
 */
const onBrandChange = () => {
    if (hasSearched.value) {
        performSearch()
    }
}

/**
 * 年份筛选变化
 */
const onYearChange = () => {
    if (hasSearched.value) {
        performSearch()
    }
}

/**
 * 学校筛选变化
 */
const onSchoolChange = () => {
    if (hasSearched.value) {
        performSearch()
    }
}

/**
 * 加载更多数据
 */
const loadMore = () => {
    if (loading.value || noMoreData.value) return

    loading.value = true
    currentPage.value++

    // 模拟API调用延迟
    setTimeout(() => {
        const mockData = generateMockData(currentPage.value, pageSize.value)

        if (mockData.length === 0 || searchResults.value.length >= totalCount.value) {
            noMoreData.value = true
        } else {
            searchResults.value.push(...mockData)
        }

        loading.value = false
    }, 500)
}

/**
 * 切换收藏状态
 * @param {string} id 商品ID
 */
const toggleFavorite = (id) => {
    const index = favoriteIds.value.indexOf(id)
    if (index > -1) {
        favoriteIds.value.splice(index, 1)
        Taro.showToast({
            title: '取消收藏',
            icon: 'none',
            duration: 2000
        })
    } else {
        favoriteIds.value.push(id)
        Taro.showToast({
            title: '收藏成功',
            icon: 'success',
            duration: 2000
        })
    }
}

/**
 * 商品点击事件
 * @param {Object} scooter 商品信息
 */
const onProductClick = (scooter) => {
    Taro.navigateTo({
        url: `/pages/productDetail/index?id=${scooter.id}`
    })
}

// 页面初始化
onMounted(() => {
    // 如果有搜索参数,自动执行搜索
    const instance = Taro.getCurrentInstance()
    if (instance.router?.params?.keyword) {
        searchValue.value = instance.router.params.keyword
        performSearch()
    }
})
</script>

<script>
export default {
    name: "SearchPage",
};
</script>