532 lines
11 KiB
Vue
532 lines
11 KiB
Vue
|
|
<template>
|
||
|
|
<view class="search-page">
|
||
|
|
<!-- 搜索框 -->
|
||
|
|
<view class="search-bar">
|
||
|
|
<view class="search-input-wrapper">
|
||
|
|
<text class="search-icon">🔍</text>
|
||
|
|
<input
|
||
|
|
class="search-input"
|
||
|
|
v-model="keyword"
|
||
|
|
placeholder="搜索商品名称"
|
||
|
|
placeholder-class="placeholder"
|
||
|
|
confirm-type="search"
|
||
|
|
@input="onInput"
|
||
|
|
@confirm="doSearch"
|
||
|
|
:focus="true"
|
||
|
|
/>
|
||
|
|
<text v-if="keyword" class="clear-icon" @click="clearKeyword">✕</text>
|
||
|
|
</view>
|
||
|
|
<text class="cancel-btn" @click="goBack">取消</text>
|
||
|
|
</view>
|
||
|
|
|
||
|
|
<!-- 搜索历史/热门搜索 -->
|
||
|
|
<view v-if="!keyword && searchResult.length === 0" class="search-suggest">
|
||
|
|
<!-- 搜索历史 -->
|
||
|
|
<view v-if="searchHistory.length > 0" class="suggest-section">
|
||
|
|
<view class="section-header">
|
||
|
|
<text class="section-title">搜索历史</text>
|
||
|
|
<text class="clear-all" @click="clearHistory">清空</text>
|
||
|
|
</view>
|
||
|
|
<view class="tags-list">
|
||
|
|
<view
|
||
|
|
v-for="(item, index) in searchHistory"
|
||
|
|
:key="index"
|
||
|
|
class="tag-item"
|
||
|
|
@click="searchByKeyword(item)"
|
||
|
|
>
|
||
|
|
{{ item }}
|
||
|
|
</view>
|
||
|
|
</view>
|
||
|
|
</view>
|
||
|
|
|
||
|
|
<!-- 热门搜索 -->
|
||
|
|
<view class="suggest-section">
|
||
|
|
<view class="section-header">
|
||
|
|
<text class="section-title">热门搜索</text>
|
||
|
|
</view>
|
||
|
|
<view class="tags-list">
|
||
|
|
<view
|
||
|
|
v-for="(item, index) in hotSearchList"
|
||
|
|
:key="index"
|
||
|
|
class="tag-item hot"
|
||
|
|
@click="searchByKeyword(item)"
|
||
|
|
>
|
||
|
|
<text v-if="index < 3" class="hot-badge">🔥</text>
|
||
|
|
{{ item }}
|
||
|
|
</view>
|
||
|
|
</view>
|
||
|
|
</view>
|
||
|
|
</view>
|
||
|
|
|
||
|
|
<!-- 搜索结果 -->
|
||
|
|
<scroll-view
|
||
|
|
v-else
|
||
|
|
class="search-result"
|
||
|
|
scroll-y
|
||
|
|
@scrolltolower="loadMore"
|
||
|
|
>
|
||
|
|
<view v-if="searchResult.length > 0" class="result-list">
|
||
|
|
<view
|
||
|
|
v-for="goods in searchResult"
|
||
|
|
:key="goods.id"
|
||
|
|
class="goods-item"
|
||
|
|
@click="goToDetail(goods.id)"
|
||
|
|
>
|
||
|
|
<image class="goods-image" :src="goods.image" mode="aspectFill"></image>
|
||
|
|
<view class="goods-info">
|
||
|
|
<text class="goods-name" v-html="highlightKeyword(goods.name)"></text>
|
||
|
|
<view class="goods-bottom">
|
||
|
|
<view class="goods-price">
|
||
|
|
<text class="points">{{ goods.points }}</text>
|
||
|
|
<text class="points-text">积分</text>
|
||
|
|
</view>
|
||
|
|
<text class="goods-sales">已兑{{ goods.sales }}件</text>
|
||
|
|
</view>
|
||
|
|
</view>
|
||
|
|
</view>
|
||
|
|
</view>
|
||
|
|
|
||
|
|
<!-- 空状态 -->
|
||
|
|
<view v-else-if="!loading && keyword" class="empty-state">
|
||
|
|
<image class="empty-image" src="/static/images/empty.png" mode="aspectFit"></image>
|
||
|
|
<text class="empty-text">没有找到相关商品</text>
|
||
|
|
<text class="empty-tip">换个关键词试试吧</text>
|
||
|
|
</view>
|
||
|
|
|
||
|
|
<!-- 加载更多 -->
|
||
|
|
<view v-if="searchResult.length > 0" class="load-more">
|
||
|
|
<text v-if="loading" class="load-text">搜索中...</text>
|
||
|
|
<text v-else-if="noMore" class="load-text">没有更多了</text>
|
||
|
|
</view>
|
||
|
|
</scroll-view>
|
||
|
|
</view>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<script>
|
||
|
|
export default {
|
||
|
|
data() {
|
||
|
|
return {
|
||
|
|
keyword: '',
|
||
|
|
searchHistory: [],
|
||
|
|
hotSearchList: [
|
||
|
|
'iPhone',
|
||
|
|
'小米手机',
|
||
|
|
'华为耳机',
|
||
|
|
'iPad',
|
||
|
|
'美的电饭煲',
|
||
|
|
'优惠券',
|
||
|
|
'零食大礼包',
|
||
|
|
'AirPods'
|
||
|
|
],
|
||
|
|
searchResult: [],
|
||
|
|
page: 1,
|
||
|
|
limit: 20,
|
||
|
|
loading: false,
|
||
|
|
noMore: false,
|
||
|
|
searchTimer: null
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
onLoad(options) {
|
||
|
|
if (options.keyword) {
|
||
|
|
this.keyword = options.keyword
|
||
|
|
this.doSearch()
|
||
|
|
}
|
||
|
|
this.loadSearchHistory()
|
||
|
|
},
|
||
|
|
|
||
|
|
methods: {
|
||
|
|
// 输入事件 - 实时搜索
|
||
|
|
onInput(e) {
|
||
|
|
this.keyword = e.detail.value
|
||
|
|
|
||
|
|
// 防抖处理
|
||
|
|
if (this.searchTimer) {
|
||
|
|
clearTimeout(this.searchTimer)
|
||
|
|
}
|
||
|
|
|
||
|
|
if (this.keyword) {
|
||
|
|
this.searchTimer = setTimeout(() => {
|
||
|
|
this.doSearch()
|
||
|
|
}, 500)
|
||
|
|
} else {
|
||
|
|
this.searchResult = []
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
// 执行搜索
|
||
|
|
async doSearch() {
|
||
|
|
if (!this.keyword.trim()) return
|
||
|
|
|
||
|
|
this.resetList()
|
||
|
|
await this.loadSearchResult()
|
||
|
|
this.saveSearchHistory()
|
||
|
|
},
|
||
|
|
|
||
|
|
// 重置列表
|
||
|
|
resetList() {
|
||
|
|
this.searchResult = []
|
||
|
|
this.page = 1
|
||
|
|
this.noMore = false
|
||
|
|
},
|
||
|
|
|
||
|
|
// 加载搜索结果
|
||
|
|
async loadSearchResult() {
|
||
|
|
if (this.loading || this.noMore) return
|
||
|
|
|
||
|
|
this.loading = true
|
||
|
|
|
||
|
|
try {
|
||
|
|
// TODO: 调用搜索API
|
||
|
|
// const res = await this.$api.integral.searchGoods({
|
||
|
|
// keyword: this.keyword,
|
||
|
|
// page: this.page,
|
||
|
|
// limit: this.limit
|
||
|
|
// })
|
||
|
|
|
||
|
|
// 模拟数据
|
||
|
|
const res = {
|
||
|
|
code: 0,
|
||
|
|
data: {
|
||
|
|
list: this.getMockGoods(),
|
||
|
|
total: 50
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (res.code === 0) {
|
||
|
|
const list = res.data.list || []
|
||
|
|
this.searchResult = this.page === 1 ? list : [...this.searchResult, ...list]
|
||
|
|
|
||
|
|
if (list.length < this.limit) {
|
||
|
|
this.noMore = true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('搜索失败:', error)
|
||
|
|
uni.showToast({
|
||
|
|
title: '搜索失败',
|
||
|
|
icon: 'none'
|
||
|
|
})
|
||
|
|
} finally {
|
||
|
|
this.loading = false
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
// 加载更多
|
||
|
|
loadMore() {
|
||
|
|
if (!this.noMore && !this.loading) {
|
||
|
|
this.page++
|
||
|
|
this.loadSearchResult()
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
// 点击搜索词
|
||
|
|
searchByKeyword(keyword) {
|
||
|
|
this.keyword = keyword
|
||
|
|
this.doSearch()
|
||
|
|
},
|
||
|
|
|
||
|
|
// 清空关键词
|
||
|
|
clearKeyword() {
|
||
|
|
this.keyword = ''
|
||
|
|
this.searchResult = []
|
||
|
|
},
|
||
|
|
|
||
|
|
// 高亮关键词
|
||
|
|
highlightKeyword(text) {
|
||
|
|
if (!this.keyword) return text
|
||
|
|
const reg = new RegExp(this.keyword, 'gi')
|
||
|
|
return text.replace(reg, `<span style="color: #FF4D4F">${this.keyword}</span>`)
|
||
|
|
},
|
||
|
|
|
||
|
|
// 保存搜索历史
|
||
|
|
saveSearchHistory() {
|
||
|
|
if (!this.keyword.trim()) return
|
||
|
|
|
||
|
|
// 去重并添加到首位
|
||
|
|
const history = this.searchHistory.filter(item => item !== this.keyword)
|
||
|
|
history.unshift(this.keyword)
|
||
|
|
|
||
|
|
// 最多保存10条
|
||
|
|
this.searchHistory = history.slice(0, 10)
|
||
|
|
|
||
|
|
// 保存到本地
|
||
|
|
uni.setStorageSync('integral_search_history', this.searchHistory)
|
||
|
|
},
|
||
|
|
|
||
|
|
// 加载搜索历史
|
||
|
|
loadSearchHistory() {
|
||
|
|
try {
|
||
|
|
const history = uni.getStorageSync('integral_search_history')
|
||
|
|
if (history) {
|
||
|
|
this.searchHistory = history
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('加载搜索历史失败:', error)
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
// 清空搜索历史
|
||
|
|
clearHistory() {
|
||
|
|
uni.showModal({
|
||
|
|
title: '提示',
|
||
|
|
content: '确定要清空搜索历史吗?',
|
||
|
|
success: (res) => {
|
||
|
|
if (res.confirm) {
|
||
|
|
this.searchHistory = []
|
||
|
|
uni.removeStorageSync('integral_search_history')
|
||
|
|
uni.showToast({
|
||
|
|
title: '已清空',
|
||
|
|
icon: 'success'
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
})
|
||
|
|
},
|
||
|
|
|
||
|
|
// 跳转商品详情
|
||
|
|
goToDetail(goodsId) {
|
||
|
|
uni.navigateTo({
|
||
|
|
url: `/pages/integral/detail?id=${goodsId}`
|
||
|
|
})
|
||
|
|
},
|
||
|
|
|
||
|
|
// 返回
|
||
|
|
goBack() {
|
||
|
|
uni.navigateBack()
|
||
|
|
},
|
||
|
|
|
||
|
|
// 模拟商品数据
|
||
|
|
getMockGoods() {
|
||
|
|
const goods = []
|
||
|
|
for (let i = 0; i < 10; i++) {
|
||
|
|
goods.push({
|
||
|
|
id: Date.now() + i,
|
||
|
|
name: `${this.keyword}商品${i + 1}`,
|
||
|
|
image: 'https://via.placeholder.com/200',
|
||
|
|
points: 1000 + i * 100,
|
||
|
|
sales: Math.floor(Math.random() * 1000)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
return goods
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<style lang="scss" scoped>
|
||
|
|
.search-page {
|
||
|
|
min-height: 100vh;
|
||
|
|
background-color: #F5F5F5;
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
}
|
||
|
|
|
||
|
|
.search-bar {
|
||
|
|
background-color: #FFFFFF;
|
||
|
|
padding: 20rpx 30rpx;
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 20rpx;
|
||
|
|
}
|
||
|
|
|
||
|
|
.search-input-wrapper {
|
||
|
|
flex: 1;
|
||
|
|
background-color: #F5F5F5;
|
||
|
|
border-radius: 40rpx;
|
||
|
|
padding: 0 30rpx;
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
height: 70rpx;
|
||
|
|
}
|
||
|
|
|
||
|
|
.search-icon {
|
||
|
|
font-size: 32rpx;
|
||
|
|
margin-right: 16rpx;
|
||
|
|
}
|
||
|
|
|
||
|
|
.search-input {
|
||
|
|
flex: 1;
|
||
|
|
font-size: 28rpx;
|
||
|
|
color: #333333;
|
||
|
|
}
|
||
|
|
|
||
|
|
.placeholder {
|
||
|
|
color: #999999;
|
||
|
|
}
|
||
|
|
|
||
|
|
.clear-icon {
|
||
|
|
width: 36rpx;
|
||
|
|
height: 36rpx;
|
||
|
|
line-height: 36rpx;
|
||
|
|
text-align: center;
|
||
|
|
font-size: 24rpx;
|
||
|
|
color: #999999;
|
||
|
|
background-color: #DDDDDD;
|
||
|
|
border-radius: 50%;
|
||
|
|
}
|
||
|
|
|
||
|
|
.cancel-btn {
|
||
|
|
font-size: 28rpx;
|
||
|
|
color: #333333;
|
||
|
|
}
|
||
|
|
|
||
|
|
.search-suggest {
|
||
|
|
padding: 30rpx;
|
||
|
|
}
|
||
|
|
|
||
|
|
.suggest-section {
|
||
|
|
margin-bottom: 40rpx;
|
||
|
|
}
|
||
|
|
|
||
|
|
.section-header {
|
||
|
|
display: flex;
|
||
|
|
justify-content: space-between;
|
||
|
|
align-items: center;
|
||
|
|
margin-bottom: 20rpx;
|
||
|
|
}
|
||
|
|
|
||
|
|
.section-title {
|
||
|
|
font-size: 30rpx;
|
||
|
|
color: #333333;
|
||
|
|
font-weight: bold;
|
||
|
|
}
|
||
|
|
|
||
|
|
.clear-all {
|
||
|
|
font-size: 26rpx;
|
||
|
|
color: #999999;
|
||
|
|
}
|
||
|
|
|
||
|
|
.tags-list {
|
||
|
|
display: flex;
|
||
|
|
flex-wrap: wrap;
|
||
|
|
gap: 20rpx;
|
||
|
|
}
|
||
|
|
|
||
|
|
.tag-item {
|
||
|
|
padding: 14rpx 28rpx;
|
||
|
|
background-color: #F5F5F5;
|
||
|
|
border-radius: 40rpx;
|
||
|
|
font-size: 26rpx;
|
||
|
|
color: #666666;
|
||
|
|
|
||
|
|
&.hot {
|
||
|
|
background-color: #FFF1F0;
|
||
|
|
color: #FF4D4F;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.hot-badge {
|
||
|
|
margin-right: 4rpx;
|
||
|
|
}
|
||
|
|
|
||
|
|
.search-result {
|
||
|
|
flex: 1;
|
||
|
|
padding: 20rpx 30rpx;
|
||
|
|
}
|
||
|
|
|
||
|
|
.result-list {
|
||
|
|
display: flex;
|
||
|
|
flex-wrap: wrap;
|
||
|
|
gap: 20rpx;
|
||
|
|
}
|
||
|
|
|
||
|
|
.goods-item {
|
||
|
|
width: calc((100% - 20rpx) / 2);
|
||
|
|
background-color: #FFFFFF;
|
||
|
|
border-radius: 16rpx;
|
||
|
|
overflow: hidden;
|
||
|
|
}
|
||
|
|
|
||
|
|
.goods-image {
|
||
|
|
width: 100%;
|
||
|
|
height: 330rpx;
|
||
|
|
background-color: #F5F5F5;
|
||
|
|
}
|
||
|
|
|
||
|
|
.goods-info {
|
||
|
|
padding: 20rpx;
|
||
|
|
}
|
||
|
|
|
||
|
|
.goods-name {
|
||
|
|
font-size: 28rpx;
|
||
|
|
color: #333333;
|
||
|
|
line-height: 1.5;
|
||
|
|
height: 84rpx;
|
||
|
|
display: -webkit-box;
|
||
|
|
-webkit-box-orient: vertical;
|
||
|
|
-webkit-line-clamp: 2;
|
||
|
|
overflow: hidden;
|
||
|
|
|
||
|
|
/deep/ span {
|
||
|
|
color: #FF4D4F;
|
||
|
|
font-weight: bold;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.goods-bottom {
|
||
|
|
display: flex;
|
||
|
|
justify-content: space-between;
|
||
|
|
align-items: flex-end;
|
||
|
|
margin-top: 16rpx;
|
||
|
|
}
|
||
|
|
|
||
|
|
.goods-price {
|
||
|
|
display: flex;
|
||
|
|
align-items: baseline;
|
||
|
|
}
|
||
|
|
|
||
|
|
.points {
|
||
|
|
font-size: 36rpx;
|
||
|
|
color: #FF4D4F;
|
||
|
|
font-weight: bold;
|
||
|
|
}
|
||
|
|
|
||
|
|
.points-text {
|
||
|
|
font-size: 24rpx;
|
||
|
|
color: #FF4D4F;
|
||
|
|
margin-left: 4rpx;
|
||
|
|
}
|
||
|
|
|
||
|
|
.goods-sales {
|
||
|
|
font-size: 24rpx;
|
||
|
|
color: #999999;
|
||
|
|
}
|
||
|
|
|
||
|
|
.empty-state {
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
padding: 200rpx 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.empty-image {
|
||
|
|
width: 300rpx;
|
||
|
|
height: 300rpx;
|
||
|
|
margin-bottom: 40rpx;
|
||
|
|
}
|
||
|
|
|
||
|
|
.empty-text {
|
||
|
|
font-size: 30rpx;
|
||
|
|
color: #333333;
|
||
|
|
margin-bottom: 16rpx;
|
||
|
|
}
|
||
|
|
|
||
|
|
.empty-tip {
|
||
|
|
font-size: 26rpx;
|
||
|
|
color: #999999;
|
||
|
|
}
|
||
|
|
|
||
|
|
.load-more {
|
||
|
|
padding: 30rpx 0;
|
||
|
|
text-align: center;
|
||
|
|
}
|
||
|
|
|
||
|
|
.load-text {
|
||
|
|
font-size: 26rpx;
|
||
|
|
color: #999999;
|
||
|
|
}
|
||
|
|
</style>
|
||
|
|
|