Files
integral-shop/single_uniapp22miao/pages/integral/search.vue
apple 079076a70e miao33: 从 main 同步 single_uniapp22miao,dart-sass 兼容修复,DEPLOY.md 更新
- 从 main 获取 single_uniapp22miao 子项目
- dart-sass: /deep/ -> ::v-deep,calc 运算符加空格
- DEPLOY.md 采用 shccd159 版本(4 子项目架构说明)

Made-with: Cursor
2026-03-16 11:16:42 +08:00

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;
::v-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>