Files
medical-mall/pages/mall/consumer/search.uvue
2026-01-23 17:31:07 +08:00

982 lines
19 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="search-page">
<!-- 搜索头部 -->
<view class="search-header" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="search-bar-container">
<!-- 返回按钮 -->
<view class="back-btn" @click="goBack">
<text class="back-icon">←</text>
</view>
<!-- 搜索框 -->
<view class="search-input-container">
<view class="search-icon">🔍</view>
<input
class="search-input"
type="text"
:value="searchKeyword"
@input="onInput"
@confirm="onSearch"
placeholder="请输入药品名称、症状或品牌"
placeholder-class="placeholder"
:focus="autoFocus"
/>
<view v-if="searchKeyword" class="clear-btn" @click="clearSearch">
<text class="clear-icon">×</text>
</view>
</view>
<!-- 搜索按钮 -->
<view class="search-btn" @click="onSearch">
<text class="search-btn-text">搜索</text>
</view>
</view>
</view>
<!-- 错误状态(模拟服务器超时) -->
<view v-if="isError" class="error-state" @click="retryLoad">
<view class="error-content">
<text class="error-icon">⚠️</text>
<text class="error-title">加载服务器超时</text>
<text class="error-desc">请点击屏幕重试</text>
</view>
</view>
<!-- 主内容区域 -->
<scroll-view
v-else
scroll-y
class="main-content"
:style="{ height: scrollHeight + 'px' }"
@scrolltolower="loadMore"
>
<!-- 初始状态(无搜索词) -->
<view v-if="!searchKeyword && !showResults">
<!-- 搜索历史 -->
<view v-if="searchHistory.length > 0" class="search-history">
<view class="section-header">
<text class="section-title">搜索历史</text>
<view class="header-right" @click="clearHistory">
<text class="clear-text">清空</text>
<text class="clear-icon-trash">🗑️</text>
</view>
</view>
<view class="history-tags">
<view
v-for="(item, index) in searchHistory"
:key="index"
class="history-tag"
@click="searchFromHistory(item)"
>
<text class="history-text">{{ item }}</text>
<view class="delete-tag-btn" @click.stop="deleteHistoryItem(index)">
<text class="delete-icon">×</text>
</view>
</view>
</view>
</view>
<!-- 热门搜索 -->
<view class="hot-search">
<view class="section-header">
<text class="section-title">热门搜索</text>
</view>
<view class="hot-tags">
<view
v-for="(item, index) in hotSearchList"
:key="index"
class="hot-tag"
:class="{ 'hot': item.hot }"
@click="searchFromHot(item.keyword)"
>
<text class="hot-rank" :class="{ 'top-three': index < 3 }">{{ index + 1 }}</text>
<text class="hot-text">{{ item.keyword }}</text>
<text v-if="item.hot" class="hot-icon">🔥</text>
</view>
</view>
</view>
<!-- 猜你需要 (新增功能) -->
<view class="guess-you-like">
<view class="section-header">
<view class="title-with-icon">
<text class="section-icon">✨</text>
<text class="section-title">猜你需要</text>
</view>
<text class="refresh-btn" @click="refreshGuessList">换一批</text>
</view>
<view class="guess-grid">
<view
v-for="item in guessList"
:key="item.id"
class="guess-item"
@click="viewProductDetail(item)"
>
<view class="guess-img-box">
<image class="guess-img" :src="item.image" mode="aspectFill" />
</view>
<view class="guess-info">
<text class="guess-name">{{ item.name }}</text>
<view class="guess-price-row">
<text class="price-symbol">¥</text>
<text class="price-num">{{ item.price }}</text>
<text class="sales-text">已售{{ item.sales }}</text>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 搜索建议 -->
<view v-if="searchKeyword && !showResults" class="search-suggestions">
<view class="suggestions-list">
<view
v-for="(suggestion, index) in searchSuggestions"
:key="index"
class="suggestion-item"
@click="selectSuggestion(suggestion)"
>
<view class="suggestion-icon">🔍</view>
<text class="suggestion-text">{{ suggestion }}</text>
</view>
</view>
</view>
<!-- 搜索结果 -->
<view v-if="showResults" class="search-results">
<view class="results-header">
<text class="results-title">搜索结果</text>
<view class="filter-tabs">
<text class="filter-tab active">综合</text>
<text class="filter-tab">销量</text>
<text class="filter-tab">价格</text>
</view>
</view>
<view v-if="searchResults.length > 0" class="results-list">
<view
v-for="product in searchResults"
:key="product.id"
class="result-item"
@click="viewProductDetail(product)"
>
<image class="product-image" :src="product.image" mode="aspectFill" />
<view class="product-info">
<text class="product-name">{{ product.name }}</text>
<view class="product-tags-row" v-if="product.tag">
<text class="product-tag">{{ product.tag }}</text>
</view>
<text class="product-spec">{{ product.specification }}</text>
<view class="product-bottom">
<view class="price-box">
<text class="price-symbol">¥</text>
<text class="price-value">{{ product.price }}</text>
</view>
<view class="add-cart-btn">
<text class="cart-icon">+</text>
</view>
</view>
</view>
</view>
</view>
<!-- 空结果 -->
<view v-if="searchResults.length === 0 && !loading" class="empty-result">
<text class="empty-icon">🤔</text>
<text class="empty-text">未找到相关商品</text>
<text class="empty-sub">换个关键词试试吧</text>
</view>
<!-- 加载更多 -->
<view v-if="loading" class="loading-more">
<view class="loading-spinner"></view>
<text class="loading-text">加载中...</text>
</view>
<view v-if="!hasMore && searchResults.length > 0" class="no-more">
<text class="no-more-text">--- 到底了 ---</text>
</view>
</view>
<!-- 底部安全区域 -->
<view class="safe-area"></view>
</scroll-view>
</view>
</template>
<script setup lang="uts">
import { ref, reactive, onMounted, computed } from 'vue'
// 状态定义
const statusBarHeight = ref(0)
const scrollHeight = ref(0)
const searchKeyword = ref('')
const showResults = ref(false)
const loading = ref(false)
const hasMore = ref(true)
const isError = ref(false) // 错误状态控制
const autoFocus = ref(true)
// 数据定义
const searchHistory = ref<string[]>([])
const hotSearchList = ref<any[]>([])
const guessList = ref<any[]>([])
const searchResults = ref<any[]>([])
// 模拟数据库
const mockDatabase = {
hot: [
{ keyword: '感冒灵', hot: true },
{ keyword: '布洛芬', hot: true },
{ keyword: '口罩', hot: true },
{ keyword: '维生素C', hot: false },
{ keyword: '板蓝根', hot: false },
{ keyword: '创可贴', hot: false },
],
guess: [
{ id: 'g1', name: '医用外科口罩', price: 19.9, image: 'https://picsum.photos/200/200?random=1', sales: '1万+' },
{ id: 'g2', name: '酒精消毒液', price: 9.9, image: 'https://picsum.photos/200/200?random=2', sales: '5000+' },
{ id: 'g3', name: '电子体温计', price: 29.9, image: 'https://picsum.photos/200/200?random=3', sales: '2000+' },
{ id: 'g4', name: '碘伏消毒液', price: 5.5, image: 'https://picsum.photos/200/200?random=4', sales: '1000+' },
{ id: 'g5', name: '退热贴', price: 15.8, image: 'https://picsum.photos/200/200?random=5', sales: '3000+' },
{ id: 'g6', name: '棉签', price: 3.9, image: 'https://picsum.photos/200/200?random=6', sales: '8000+' },
]
}
// 搜索建议
const searchSuggestions = computed(() => {
if (!searchKeyword.value) return []
// 简单模拟
return [
`${searchKeyword.value}胶囊`,
`${searchKeyword.value}颗粒`,
`${searchKeyword.value}片`,
`儿童${searchKeyword.value}`
]
})
onMounted(() => {
initPage()
})
const initPage = () => {
try {
const systemInfo = uni.getSystemInfoSync()
statusBarHeight.value = systemInfo.statusBarHeight || 0
const windowHeight = systemInfo.windowHeight
// 减去头部高度 (约60px + statusBarHeight)
scrollHeight.value = windowHeight - (60 + statusBarHeight.value)
loadData()
} catch (e) {
console.error('初始化失败', e)
isError.value = true
}
}
// 加载基础数据
const loadData = () => {
loading.value = true
isError.value = false
// 模拟网络请求
setTimeout(() => {
try {
loadSearchHistory()
hotSearchList.value = mockDatabase.hot
guessList.value = mockDatabase.guess
loading.value = false
} catch (e) {
isError.value = true
loading.value = false
}
}, 500)
}
// 点击重试
const retryLoad = () => {
uni.showLoading({ title: '重新加载中' })
setTimeout(() => {
uni.hideLoading()
loadData()
}, 1000)
}
// 历史记录管理
const loadSearchHistory = () => {
const history = uni.getStorageSync('searchHistory')
if (history) {
try {
// 确保是数组
const parsed = JSON.parse(history as string)
if (Array.isArray(parsed)) {
searchHistory.value = parsed as string[]
}
} catch (e) {
searchHistory.value = []
}
}
}
const saveSearchHistory = () => {
uni.setStorageSync('searchHistory', JSON.stringify(searchHistory.value))
}
const addToHistory = (keyword: string) => {
if (!keyword) return
const index = searchHistory.value.indexOf(keyword)
if (index > -1) {
searchHistory.value.splice(index, 1)
}
searchHistory.value.unshift(keyword)
if (searchHistory.value.length > 10) searchHistory.value.pop()
saveSearchHistory()
}
const clearHistory = () => {
uni.showModal({
title: '提示',
content: '确定清空搜索历史吗?',
success: (res) => {
if (res.confirm) {
searchHistory.value = []
uni.removeStorageSync('searchHistory')
}
}
})
}
const deleteHistoryItem = (index: number) => {
searchHistory.value.splice(index, 1)
saveSearchHistory()
}
// 搜索逻辑
const onInput = (e: any) => {
searchKeyword.value = e.detail.value
if (!searchKeyword.value) {
showResults.value = false
}
}
const clearSearch = () => {
searchKeyword.value = ''
showResults.value = false
}
const onSearch = () => {
if (!searchKeyword.value.trim()) return
addToHistory(searchKeyword.value.trim())
performSearch()
}
const searchFromHistory = (keyword: string) => {
searchKeyword.value = keyword
performSearch()
}
const searchFromHot = (keyword: string) => {
searchKeyword.value = keyword
addToHistory(keyword)
performSearch()
}
const selectSuggestion = (suggestion: string) => {
searchKeyword.value = suggestion
addToHistory(suggestion)
performSearch()
}
const performSearch = () => {
showResults.value = true
loading.value = true
searchResults.value = [] // 清空旧结果
// 模拟搜索请求
setTimeout(() => {
loading.value = false
// 生成模拟结果
searchResults.value = Array.from({ length: 6 }, (_, i) => ({
id: `s${i}`,
name: `${searchKeyword.value}相关药品-${i+1}`,
specification: '10g*12袋',
price: (Math.random() * 50 + 10).toFixed(1),
image: `https://picsum.photos/300/300?random=s${i}`,
sales: Math.floor(Math.random() * 1000),
tag: i % 2 === 0 ? '自营' : ''
}))
hasMore.value = true
}, 800)
}
const loadMore = () => {
if (loading.value || !hasMore.value) return
loading.value = true
setTimeout(() => {
const newItems = Array.from({ length: 4 }, (_, i) => ({
id: `more${Date.now()}${i}`,
name: `${searchKeyword.value}更多药品-${i+1}`,
specification: '盒装',
price: (Math.random() * 50 + 10).toFixed(1),
image: `https://picsum.photos/300/300?random=m${i}`,
sales: Math.floor(Math.random() * 500),
tag: ''
}))
searchResults.value.push(...newItems)
loading.value = false
if (searchResults.value.length > 20) hasMore.value = false
}, 1000)
}
const refreshGuessList = () => {
uni.showLoading({ title: '刷新中' })
setTimeout(() => {
guessList.value = guessList.value.sort(() => Math.random() - 0.5)
uni.hideLoading()
}, 500)
}
const viewProductDetail = (item: any) => {
// 跳转详情页逻辑
console.log('查看商品', item)
uni.showToast({ title: '点击了商品: ' + item.name, icon: 'none' })
}
const goBack = () => {
uni.navigateBack()
}
</script>
<style>
.search-page {
width: 100%;
height: 100vh;
background-color: #f5f5f5;
display: flex;
flex-direction: column;
}
/* 头部样式 */
.search-header {
background-color: #ffffff;
padding-bottom: 10px;
position: sticky;
top: 0;
z-index: 100;
}
.search-bar-container {
display: flex;
align-items: center;
padding: 10px 16px;
gap: 12px;
}
.back-btn {
padding: 4px;
}
.back-icon {
font-size: 24px;
color: #333;
}
.search-input-container {
flex: 1;
height: 36px;
background-color: #f0f0f0;
border-radius: 18px;
display: flex;
align-items: center;
padding: 0 12px;
}
.search-icon {
font-size: 16px;
margin-right: 8px;
color: #999;
}
.search-input {
flex: 1;
font-size: 14px;
color: #333;
height: 100%;
}
.placeholder {
color: #999;
}
.clear-btn {
padding: 4px;
}
.clear-icon {
font-size: 16px;
color: #999;
}
.search-btn {
padding: 4px 0;
}
.search-btn-text {
font-size: 15px;
color: #4CAF50;
font-weight: 500;
}
/* 内容区域 */
.main-content {
flex: 1;
padding: 12px;
box-sizing: border-box;
}
/* 模块通用头部 */
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
margin-top: 8px;
}
.section-title {
font-size: 15px;
font-weight: bold;
color: #333;
}
.header-right {
display: flex;
align-items: center;
gap: 4px;
}
.clear-text {
font-size: 12px;
color: #999;
}
.clear-icon-trash {
font-size: 14px;
}
/* 搜索历史 */
.search-history {
margin-bottom: 24px;
}
.history-tags {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.history-tag {
background-color: #fff;
padding: 6px 12px;
border-radius: 16px;
display: flex;
align-items: center;
gap: 6px;
max-width: 100%;
}
.history-text {
font-size: 13px;
color: #666;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.delete-tag-btn {
display: flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
border-radius: 50%;
background-color: #f0f0f0;
}
.delete-icon {
font-size: 12px;
color: #999;
line-height: 1;
}
/* 热门搜索 */
.hot-search {
margin-bottom: 24px;
}
.hot-tags {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.hot-tag {
background-color: #fff;
padding: 6px 12px;
border-radius: 4px;
display: flex;
align-items: center;
gap: 4px;
}
.hot-tag.hot {
background-color: #fff0f0;
}
.hot-rank {
font-size: 12px;
color: #999;
font-weight: bold;
margin-right: 2px;
}
.hot-rank.top-three {
color: #ff5000;
}
.hot-text {
font-size: 13px;
color: #333;
}
.hot-icon {
font-size: 12px;
}
/* 猜你需要 */
.guess-you-like {
margin-bottom: 20px;
}
.title-with-icon {
display: flex;
align-items: center;
gap: 6px;
}
.section-icon {
font-size: 16px;
}
.refresh-btn {
font-size: 12px;
color: #4CAF50;
}
.guess-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
.guess-item {
background-color: #fff;
border-radius: 8px;
overflow: hidden;
padding-bottom: 8px;
}
.guess-img-box {
width: 100%;
height: 0;
padding-bottom: 100%;
position: relative;
background-color: #f0f0f0;
}
.guess-img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.guess-info {
padding: 8px;
}
.guess-name {
font-size: 13px;
color: #333;
margin-bottom: 6px;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
.guess-price-row {
display: flex;
align-items: baseline;
}
.price-symbol {
font-size: 12px;
color: #ff5000;
}
.price-num {
font-size: 16px;
color: #ff5000;
font-weight: bold;
margin-right: 6px;
}
.sales-text {
font-size: 10px;
color: #999;
}
/* 搜索建议列表 */
.search-suggestions {
background-color: #fff;
border-radius: 8px;
padding: 0 12px;
}
.suggestion-item {
display: flex;
align-items: center;
height: 44px;
border-bottom: 1px solid #f5f5f5;
}
.suggestion-icon {
margin-right: 10px;
font-size: 14px;
color: #999;
}
.suggestion-text {
font-size: 14px;
color: #333;
}
/* 搜索结果 */
.search-results {
padding-bottom: 20px;
}
.results-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.results-title {
font-size: 15px;
font-weight: bold;
color: #333;
}
.filter-tabs {
display: flex;
gap: 16px;
}
.filter-tab {
font-size: 13px;
color: #666;
}
.filter-tab.active {
color: #4CAF50;
font-weight: 500;
}
.results-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.result-item {
background-color: #fff;
border-radius: 8px;
padding: 10px;
display: flex;
gap: 12px;
}
.product-image {
width: 100px;
height: 100px;
border-radius: 4px;
background-color: #f0f0f0;
}
.product-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.product-name {
font-size: 15px;
color: #333;
font-weight: 500;
line-height: 1.4;
}
.product-tags-row {
margin-top: 4px;
}
.product-tag {
font-size: 10px;
color: #ff5000;
border: 1px solid #ff5000;
padding: 1px 4px;
border-radius: 2px;
}
.product-spec {
font-size: 12px;
color: #999;
margin-top: 4px;
}
.product-bottom {
display: flex;
justify-content: space-between;
align-items: flex-end;
margin-top: 8px;
}
.price-box {
color: #ff5000;
}
.price-value {
font-size: 18px;
font-weight: bold;
}
.add-cart-btn {
width: 24px;
height: 24px;
background-color: #4CAF50;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.cart-icon {
color: #fff;
font-size: 16px;
font-weight: bold;
}
/* 错误状态 */
.error-state {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
background-color: #fff;
}
.error-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.error-icon {
font-size: 48px;
}
.error-title {
font-size: 16px;
font-weight: bold;
color: #333;
}
.error-desc {
font-size: 14px;
color: #999;
}
/* 加载更多 */
.loading-more {
padding: 20px 0;
display: flex;
flex-direction: column;
align-items: center;
}
.loading-spinner {
width: 24px;
height: 24px;
border: 2px solid #f0f0f0;
border-top-color: #4CAF50;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 8px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-text {
font-size: 12px;
color: #999;
}
.no-more {
padding: 20px 0;
text-align: center;
}
.no-more-text {
font-size: 12px;
color: #ccc;
}
.empty-result {
display: flex;
flex-direction: column;
align-items: center;
padding: 40px 0;
}
.empty-icon {
font-size: 40px;
margin-bottom: 12px;
}
.empty-text {
font-size: 14px;
color: #666;
margin-bottom: 4px;
}
.empty-sub {
font-size: 12px;
color: #999;
}
.safe-area {
height: 20px;
}
</style>