miao33: 从 main 同步 single_uniapp22miao,dart-sass 兼容修复,DEPLOY.md 更新
- 从 main 获取 single_uniapp22miao 子项目 - dart-sass: /deep/ -> ::v-deep,calc 运算符加空格 - DEPLOY.md 采用 shccd159 版本(4 子项目架构说明) Made-with: Cursor
This commit is contained in:
531
single_uniapp22miao/pages/integral/search.vue
Normal file
531
single_uniapp22miao/pages/integral/search.vue
Normal file
@@ -0,0 +1,531 @@
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user