Files
medical-mall/pages/mall/consumer/search.uvue

1487 lines
34 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">&lt;</text>
</view>
<!-- 搜索框 -->
<view class="search-input-container">
<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 class="camera-btn" @click="openCamera">
<text class="camera-icon">📷</text>
</view>
<!-- 搜索按钮:移入输入框内部 -->
<view class="inner-search-btn" @click="onSearch">
<text class="inner-search-text">搜索</text>
</view>
</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
direction="vertical"
class="main-content"
:style="{ height: scrollHeight + 'px' }"
@scrolltolower="loadMore"
>
<!-- 初始状态(无搜索词) -->
<view v-if="searchKeyword == '' && showResults == false">
<!-- 搜索历史 -->
<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="item.hot == true ? 'hot' : ''"
@click="searchFromHot(item.keyword)"
>
<text class="hot-rank" :class="index < 3 ? 'top-three' : ''">{{ index + 1 }}</text>
<text class="hot-text">{{ item.keyword }}</text>
<text v-if="item.hot == true" 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 == false" 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 v-if="searchShopResults.length > 0" class="shop-results-section">
<view class="section-top">
<text class="result-title-sm">相关店铺</text>
</view>
<scroll-view direction="horizontal" class="shop-list-scroll">
<view class="shop-list-row">
<view
v-for="shop in searchShopResults"
:key="shop.id"
class="shop-card"
@click="viewShopDetail(shop)"
>
<image class="shop-logo" :src="shop.logo" mode="aspectFill" />
<view class="shop-info">
<text class="shop-name-txt">{{ shop.name }}</text>
<text class="shop-products-txt">共{{ shop.productCount }}件商品</text>
</view>
</view>
</view>
</scroll-view>
</view>
<view class="results-header">
<text class="results-title">商品结果</text>
<view class="filter-tabs">
<text
class="filter-tab"
:class="{ active: activeSort === 'default' }"
@click="switchSort('default')"
>综合</text>
<text
class="filter-tab"
:class="{ active: activeSort === 'sales' }"
@click="switchSort('sales')"
>销量</text>
<text
class="filter-tab"
:class="{ active: activeSort === 'price' }"
@click="switchSort('price')"
>
价格 {{ activeSort === 'price' ? (priceSortAsc ? '↑' : '↓') : '' }}
</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" @click.stop="addToCart(product)">
<text class="cart-icon">+</text>
</view>
</view>
</view>
</view>
</view>
<!-- 空结果 - 仅在非加载状态且无结果时显示 -->
<view v-if="!loading && searchResults.length === 0" 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'
import { supabaseService } from '@/utils/supabaseService.uts'
import type { Product } from '@/utils/supabaseService.uts'
// 状态定义
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 activeSort = ref('default') // 当前排序方式: default, sales, price
const priceSortAsc = ref(false) // 价格排序是否为升序
type HotSearchItemType = {
keyword: string
hot: boolean
}
type GuessItemType = {
id: string
name: string
price: number
image: string
sales: number
}
type SearchResultType = {
id: string
name: string
image: string
price: number
specification: string
tag: string
sales: number
}
type ShopResultType = {
id: string
name: string
logo: string
productCount: number
}
const searchHistory = ref<Array<string>>([])
const hotSearchList = ref<Array<HotSearchItemType>>([])
const guessList = ref<Array<GuessItemType>>([])
const allGuessItems = ref<Array<GuessItemType>>([])
const searchResults = ref<Array<SearchResultType>>([])
const searchShopResults = ref<Array<ShopResultType>>([])
const loadSearchHistory = () => {
const history = uni.getStorageSync('searchHistory')
if (history != null) {
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 refreshGuessListItems = () => {
if (allGuessItems.value.length > 0) {
const arr: Array<GuessItemType> = []
for (let i: number = 0; i < allGuessItems.value.length; i++) {
arr.push(allGuessItems.value[i])
}
for (let i: number = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1))
const temp = arr[i]
arr[i] = arr[j]
arr[j] = temp
}
const result: Array<GuessItemType> = []
const limit = arr.length < 6 ? arr.length : 6
for (let i: number = 0; i < limit; i++) {
result.push(arr[i])
}
guessList.value = result
}
}
const loadData = async (): Promise<void> => {
isError.value = false
try {
loadSearchHistory()
const hotProducts = await supabaseService.getHotProducts(30)
const hotList: Array<HotSearchItemType> = []
const limit1 = hotProducts.length < 10 ? hotProducts.length : 10
for (let i: number = 0; i < limit1; i++) {
const p = hotProducts[i] as UTSJSONObject
const item: HotSearchItemType = {
keyword: p.getString('name') ?? '',
hot: true
}
hotList.push(item)
}
hotSearchList.value = hotList
const allItems: Array<GuessItemType> = []
for (let i: number = 0; i < hotProducts.length; i++) {
const p = hotProducts[i] as UTSJSONObject
const saleCount = p.getNumber('sale_count')
const item: GuessItemType = {
id: p.getString('id') ?? '',
name: p.getString('name') ?? '',
price: p.getNumber('base_price') ?? 0,
image: p.getString('main_image_url') ?? '/static/default.jpg',
sales: saleCount != null ? saleCount : 0
}
allItems.push(item)
}
allGuessItems.value = allItems
refreshGuessListItems()
} catch (e) {
console.error('Load data failed', e)
isError.value = true
}
}
const retryLoad = () => {
uni.showLoading({ title: '重新加载中' })
setTimeout(() => {
uni.hideLoading()
loadData()
}, 1000)
}
const searchSuggestions = ref<Array<string>>([])
let suggestTimer: number = 0
const fetchSuggestions = async (kw: string): Promise<void> => {
if (kw == '' || showResults.value) return
try {
const res = await supabaseService.searchProducts(kw.trim(), 1, 5)
if (res.data != null && res.data.length > 0) {
const names: Array<string> = []
for (let i: number = 0; i < res.data.length; i++) {
const p = res.data[i]
let name = ''
if (p instanceof UTSJSONObject) {
name = p.getString('name') ?? ''
} else {
const pObj = p as UTSJSONObject
name = pObj.getString('name') ?? ''
}
let found = false
for (let j: number = 0; j < names.length; j++) {
if (names[j] === name) {
found = true
break
}
}
if (found === false && name !== '') {
names.push(name)
}
}
searchSuggestions.value = names
} else {
searchSuggestions.value = []
}
} catch(e) {
searchSuggestions.value = []
}
}
const currentPage = ref<number>(1)
const performSearch = async (): Promise<void> => {
showResults.value = true
loading.value = true
currentPage.value = 1
const keyword = searchKeyword.value.trim()
if (keyword == '') {
loading.value = false
return
}
let sortBy = 'sales'
let ascending = false
if (activeSort.value === 'price') {
sortBy = 'price'
ascending = priceSortAsc.value
} else if (activeSort.value === 'default') {
sortBy = 'default'
}
try {
const prodResp = await supabaseService.searchProducts(keyword, currentPage.value, 20, sortBy, ascending)
let shopRespData: Array<any> = []
if (currentPage.value === 1 && activeSort.value === 'default') {
const shopResp = await supabaseService.searchShops(keyword)
if (shopResp.data != null) {
const rawData = shopResp.data
for (let i: number = 0; i < rawData.length; i++) {
shopRespData.push(rawData[i])
}
}
}
if (shopRespData.length > 0) {
const shopList: Array<ShopResultType> = []
for (let i: number = 0; i < shopRespData.length; i++) {
const s = shopRespData[i] as UTSJSONObject
const shopItem: ShopResultType = {
id: s.getString('id') ?? '',
name: s.getString('shop_name') ?? '',
logo: s.getString('shop_logo') ?? '/static/shop_logo_default.png',
productCount: s.getNumber('product_count') ?? 0
}
shopList.push(shopItem)
}
searchShopResults.value = shopList
} else {
searchShopResults.value = []
}
const prodData = prodResp.data != null ? prodResp.data : []
const resultList: Array<SearchResultType> = []
for (let i: number = 0; i < prodData.length; i++) {
const p = prodData[i] as UTSJSONObject
let tag = ''
const tagsRaw = p.get('tags')
if (tagsRaw != null) {
try {
const tagsStr = p.getString('tags')
if (tagsStr != null) {
const tags = JSON.parse(tagsStr)
if (Array.isArray(tags) && tags.length > 0) {
const firstTag = tags[0]
tag = firstTag != null ? (firstTag as string) : ''
}
}
} catch(e) {}
}
const searchItem: SearchResultType = {
id: p.getString('id') ?? '',
name: p.getString('name') ?? '',
image: p.getString('main_image_url') ?? '/static/default.jpg',
price: p.getNumber('base_price') ?? 0,
specification: p.getString('specification') ?? '标准规格',
tag: tag,
sales: p.getNumber('sale_count') ?? 0
}
resultList.push(searchItem)
}
searchResults.value = resultList
hasMore.value = prodResp.hasmore
} catch(e) {
console.error('Search failed', e)
} finally {
loading.value = false
}
}
const initPage = () => {
try {
const systemInfo = uni.getSystemInfoSync()
statusBarHeight.value = systemInfo.statusBarHeight ?? 0
const windowHeight = systemInfo.windowHeight
scrollHeight.value = windowHeight - (60 + statusBarHeight.value)
loadData()
const pages = getCurrentPages()
if (pages.length > 0) {
const currentPageObj = pages[pages.length - 1]
// @ts-ignore
const options = currentPageObj.options
if (options != null) {
const optObj = options as UTSJSONObject
const kwRaw = optObj.getString('keyword')
if (kwRaw != null && kwRaw !== '') {
const decoded = decodeURIComponent(kwRaw)
const keyword = decoded != null ? decoded : kwRaw
searchKeyword.value = keyword
const typeVal = optObj.getString('type')
if (typeVal === 'family' || typeVal === 'brand') {
if (typeVal === 'family') {
addToHistory(keyword)
}
showResults.value = true
loading.value = true
performSearch()
}
}
}
}
} catch (e) {
console.error('初始化失败', e)
isError.value = true
}
}
onMounted(() => {
initPage()
})
const onInput = (e: any) => {
const eObj = e as UTSJSONObject
const detailRaw = eObj.get('detail')
const detail = detailRaw != null ? (detailRaw as UTSJSONObject) : (new UTSJSONObject())
const val = detail.getString('value') ?? ''
searchKeyword.value = val
if (val == '') {
showResults.value = false
searchSuggestions.value = []
return
}
if (suggestTimer > 0) clearTimeout(suggestTimer)
suggestTimer = setTimeout(() => {
fetchSuggestions(val)
}, 300)
}
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 switchSort = (type: string) => {
if (type === 'price') {
if (activeSort.value === 'price') {
priceSortAsc.value = !priceSortAsc.value
} else {
activeSort.value = 'price'
priceSortAsc.value = true // 默认升序
}
} else {
activeSort.value = type
}
// 重新执行搜索以获取正确排序的数据
performSearch()
}
const loadMore = async (): Promise<void> => {
if (loading.value || hasMore.value == false || searchKeyword.value.trim() == '') return
loading.value = true
currentPage.value++
const keyword = searchKeyword.value.trim()
let sortBy = 'sales'
let ascending = false
if (activeSort.value === 'price') {
sortBy = 'price'
ascending = priceSortAsc.value
} else if (activeSort.value === 'default') {
sortBy = 'default'
}
try {
const response = await supabaseService.searchProducts(keyword, currentPage.value, 20, sortBy, ascending)
const respData = response.data != null ? response.data : []
for (let i: number = 0; i < respData.length; i++) {
const p = respData[i] as UTSJSONObject
let tag = ''
const tagsRaw = p.get('tags')
if (tagsRaw != null) {
try {
const tagsStr = p.getString('tags')
if (tagsStr != null) {
const tags = JSON.parse(tagsStr)
if (Array.isArray(tags) && tags.length > 0) {
const firstTag = tags[0]
tag = firstTag != null ? (firstTag as string) : ''
}
}
} catch(e) {}
}
const searchItem: SearchResultType = {
id: p.getString('id') ?? '',
name: p.getString('name') ?? '',
image: p.getString('main_image_url') ?? '/static/default.jpg',
price: p.getNumber('base_price') ?? 0,
specification: p.getString('specification') ?? '标准规格',
tag: tag,
sales: p.getNumber('sale_count') ?? 0
}
searchResults.value.push(searchItem)
}
hasMore.value = response.hasmore
} catch(e) {
console.error('Load more failed', e)
hasMore.value = false
} finally {
loading.value = false
}
}
const refreshGuessList = () => {
uni.showLoading({ title: '刷新中' })
setTimeout(() => {
refreshGuessListItems()
uni.hideLoading()
}, 500)
}
const viewProductDetail = (item: SearchResultType | GuessItemType) => {
const id = (item as GuessItemType).id
const price = (item as GuessItemType).price
const name = (item as GuessItemType).name
uni.navigateTo({
url: `/pages/mall/consumer/product-detail?productId=${id}&price=${price}&name=${encodeURIComponent(name)}`
})
}
const viewShopDetail = (shop: ShopResultType) => {
uni.navigateTo({
url: `/pages/mall/consumer/shop-detail?id=${shop.id}`
})
}
const addToCart = (product: SearchResultType | GuessItemType) => {
uni.showToast({ title: '请选择规格', icon: 'none' })
setTimeout(() => {
viewProductDetail(product)
}, 800)
}
const openCamera = () => {
uni.chooseImage({
count: 1,
sourceType: ['camera'],
success: (res) => {
console.log('拍摄图片路径:', res.tempFilePaths[0])
uni.showToast({ title: '已启用相机', icon: 'none' })
},
fail: (err) => {
console.error('启用相机失败', err)
}
})
}
const goBack = () => {
if (showResults.value) {
// 如果在搜索结果页,先返回到搜索初始页
showResults.value = false
searchKeyword.value = ''
} else {
// 如果在搜索初始页,则返回上一页
const pages = getCurrentPages()
if (pages.length > 1) {
uni.navigateBack()
} else {
// 如果只有一页(由于深链接或重定向),返回首页
uni.switchTab({
url: '/pages/mall/consumer/index'
})
}
}
}
</script>
<style>
.search-page {
width: 100%;
flex: 1; /* Fixed 100vh */
background-color: #f5f5f5;
display: flex;
flex-direction: column;
}
/* 店铺搜索结果 */
.shop-results-section {
background-color: #fff;
margin-bottom: 10px;
padding: 10px 0;
}
.section-top {
padding: 0 12px 10px;
}
.result-title-sm {
font-size: 14px;
font-weight: bold;
color: #333;
}
.shop-list-scroll {
width: 100%;
white-space: nowrap;
}
.shop-list-row {
display: flex;
flex-direction: row;
padding: 0 12px;
}
.shop-card {
display: flex;
flex-direction: column;
align-items: center;
width: 80px;
margin-right: 15px;
background-color: #f9f9f9;
padding: 10px 5px;
border-radius: 8px;
}
.shop-logo {
width: 48px;
height: 48px;
border-radius: 24px;
margin-bottom: 5px;
border: 1px solid #f0f0f0;
background-color: white;
}
.shop-info {
width: 100%;
text-align: center;
}
.shop-name-txt {
font-size: 12px;
color: #333;
width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
/* display: block; REMOVED */
margin-bottom: 2px;
}
.shop-products-txt {
font-size: 10px;
color: #999;
}
/* 头部样式 */
.search-header {
background-color: #ffffff;
padding-bottom: 10px;
/* #ifdef APP-PLUS */
padding-top: 0; /* 在App端由style动态控制 */
/* #endif */
}
.search-bar-container {
display: flex;
flex-direction: row; /* UVUE 必须显式设置 row */
align-items: center;
padding: 10px 16px;
width: 100%; /* 确保占满宽度 */
}
.back-btn {
padding: 4px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
width: 32px; /* 固定宽度防止压缩 */
height: 32px;
margin-right: 12px;
}
.back-icon {
font-size: 24px;
color: #333;
font-weight: bold;
font-family: monospace;
}
.search-input-container {
flex: 1; /* 占据剩余空间 */
height: 40px; /*稍微增高一点以容纳按钮*/
background-color: #f0f0f0;
border-radius: 20px;
display: flex;
flex-direction: row; /* UVUE 必须显式设置 row */
align-items: center;
padding: 0 4px 0 12px;
}
.search-input {
flex: 1;
font-size: 14px;
color: #333;
height: 100%;
background-color: transparent; /* 确保背景透明 */
}
.placeholder {
color: #999;
}
.clear-btn {
padding: 4px;
margin-right: 2px;
display: flex;
align-items: center;
justify-content: center;
}
.clear-icon {
font-size: 16px;
color: #999;
}
.camera-btn {
padding: 4px 8px 4px 4px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
border-right-width: 1px; /* UVUE 边框写法 */
border-right-style: solid;
border-right-color: #ddd;
margin-right: 8px;
}
.camera-icon {
font-size: 20px;
}
/* 内部搜索按钮样式 */
.inner-search-btn {
padding: 0 16px;
background-color: #87CEEB;
border-radius: 16px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
height: 32px;
}
.inner-search-text {
font-size: 13px;
color: #ffffff;
font-weight: bold;
}
/* 内容区域 */
.main-content {
flex: 1;
padding: 12px;
box-sizing: border-box;
}
/* 模块通用头部 */
.section-header {
display: flex;
flex-direction: row; /* UVUE 显式设置 row */
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
margin-top: 8px;
width: 100%;
}
.section-title {
font-size: 15px;
font-weight: bold;
color: #333;
flex: 1; /* 占据左侧空间 */
}
.header-right {
display: flex;
flex-direction: row; /* UVUE 显式设置 row */
align-items: center;
/* gap: 4px; REMOVED */
flex-shrink: 0; /* 防止被压缩 */
}
.clear-text {
margin-right: 4px; /* REPLACED gap */
font-size: 12px;
color: #999;
}
.clear-icon-trash {
font-size: 14px;
}
/* 搜索历史 */
.search-history {
margin-bottom: 24px;
padding: 0 4px; /* 微调内边距 */
}
.history-tags {
display: flex;
flex-direction: row; /* UVUE 显式设置 row */
/* gap: 10px; REMOVED */
flex-wrap: wrap; /* 允许换行 */
padding: 0 4px;
align-items: center;
}
.history-tag {
background-color: #fff;
padding: 6px 12px;
border-radius: 16px;
display: flex;
align-items: center;
/* gap: 6px; REMOVED */
flex-shrink: 0; /* 防止被压缩 */
margin-right: 10px; /* REPLACED gap */
margin-bottom: 10px; /* REPLACED gap */
}
.history-text {
font-size: 13px;
color: #666;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-right: 6px; /* REPLACED gap */
}
.delete-tag-btn {
display: flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
border-radius: 8px;
background-color: #f0f0f0;
}
.delete-icon {
font-size: 12px;
color: #999;
line-height: 1;
}
/* 热门搜索 */
.hot-search {
margin-bottom: 24px;
}
.hot-tags {
display: flex;
flex-direction: row; /* UVUE 显式设置 row */
flex-wrap: wrap; /* 允许换行 */
/* gap: 10px; REMOVED */
padding: 0 4px;
}
.hot-tag {
/* ... existing styles ... */
margin-right: 10px; /* REPLACED gap */
margin-bottom: 10px; /* REPLACED gap */
}
.hot-tag {
background-color: #fff;
padding: 6px 12px;
border-radius: 16px; /* 增加圆角,像胶囊一样 */
display: flex;
flex-direction: row;
align-items: center;
flex-shrink: 0; /* 防止被压缩 */
margin-right: 10px; /* REPLACED gap */
margin-bottom: 10px; /* REPLACED gap */
}
.hot-tag.hot {
background-color: #fff0f0;
}
.hot-rank {
font-size: 12px;
color: #999;
font-weight: bold;
margin-right: 6px;
}
.hot-rank.top-three {
color: #ff5000;
}
.hot-text {
font-size: 13px;
color: #333;
}
.hot-icon {
font-size: 12px;
margin-left: 4px;
}
/* 猜你需要 */
.guess-you-like {
margin-bottom: 20px;
}
.title-with-icon {
display: flex;
align-items: center;
}
.section-icon {
font-size: 16px;
margin-right: 6px;
}
.refresh-btn {
font-size: 12px;
color: #4CAF50;
}
.guess-grid {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
padding: 0 4px;
}
.guess-item {
background-color: #fff;
border-radius: 8px;
overflow: hidden;
padding-bottom: 8px;
width: 48%;
margin-bottom: 10px;
}
.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;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.3;
height: 34px; /* 限制2行高度 */
}
.guess-price-row {
display: flex;
align-items: flex-end; /* REPLACED 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;
flex-direction: row; /* UVUE 显式设置 row */
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
flex-wrap: wrap; /* 允许换行以适应小屏 */
}
.results-title {
font-size: 15px;
font-weight: bold;
color: #333;
margin-right: 8px;
}
.filter-tabs {
display: flex;
flex-direction: row; /* UVUE 显式设置 row */
flex: 1; /* 自适应填充剩余空间 */
justify-content: flex-end; /* 靠右对齐 */
}
.filter-tab {
font-size: 13px;
color: #666;
padding: 4px 8px; /* 增加点击区域 */
margin-left: 16px;
}
.filter-tab.active {
color: #4CAF50;
font-weight: bold; /* REPLACED 500 */
}
.results-list {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
padding: 0 4px;
}
/* 响应式布局 */
/* 平板设备 (768px以上) */
@media screen and (min-width: 768px) {
.results-list {
padding: 0 16px;
}
.result-item {
width: 32%;
}
.guess-item {
width: 24%; /* 猜你喜欢在平板上显示4列 */
}
}
/* 桌面设备 (1024px以上) */
@media screen and (min-width: 1024px) {
.results-list {
padding: 0 24px;
}
.result-item {
width: 24%;
}
.guess-item {
width: 16%; /* 猜你喜欢在桌面上显示6列 */
}
/* 桌面端调整图片高度 */
.product-image {
height: 160px;
}
}
.result-item {
background-color: #fff;
border-radius: 8px;
padding: 8px;
display: flex;
flex-direction: column; /* 垂直排列 */
width: 48%;
margin-bottom: 10px;
}
.product-image {
width: 100%;
height: 120px; /* 调整图片高度 */
border-radius: 4px;
background-color: #f0f0f0;
}
.product-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
margin-top: 8px;
}
.product-name {
font-size: 13px; /* 减小字号 */
color: #333;
font-weight: bold;
line-height: 1.3;
height: 34px; /* 限制高度 */
overflow: hidden;
text-overflow: ellipsis;
}
.product-tags-row {
margin-top: 2px;
display: none; /* 隐藏标签以保持简洁 */
}
.product-spec {
display: none; /* 隐藏规格 */
}
.product-bottom {
display: flex;
justify-content: space-between;
align-items: center; /* 垂直居中 */
margin-top: 4px;
}
.price-box {
color: #ff5000;
display: flex;
align-items: flex-end;
}
.price-symbol {
font-size: 10px;
}
.price-value {
font-size: 16px; /* 减小价格字号 */
font-weight: 700;
}
.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: 14px;
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;
}
.error-icon {
font-size: 48px;
margin-bottom: 12px;
}
.error-title {
font-size: 16px;
font-weight: bold;
color: #333;
margin-bottom: 12px;
}
.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: 12px;
margin-bottom: 8px;
}
.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>