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

1560 lines
37 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">
<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>
<!-- 主内容区域:改为普通 view由页面整体滚动 -->
<view
v-else
class="main-content"
>
<!-- 初始状态(无搜索词) -->
<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)"
>
<image class="guess-img" :src="item.image" mode="aspectFill" />
<text class="guess-name" :lines="2">{{ item.name }}</text>
<view class="guess-bottom">
<text class="guess-price">¥{{ item.price }}</text>
<view class="guess-add-btn" @click.stop="addToCart(item)">
<text class="guess-add-icon">+</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" />
<text class="product-name" :lines="2">{{ product.name }}</text>
<view class="product-bottom">
<text class="product-price">¥{{ product.price }}</text>
<view class="product-add-btn" @click.stop="addToCart(product)">
<text class="add-icon">+</text>
</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>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, reactive, onMounted, computed } from 'vue'
import { onPageScroll, onReachBottom } from '@dcloudio/uni-app'
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
merchant_id: string
}
type SearchResultType = {
id: string
name: string
image: string
price: number
specification: string
tag: string
sales: number
merchant_id: string
}
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()
// 获取热销商品,失败时使用空数组
let hotProducts: Product[] = []
try {
const hotResult = await supabaseService.getHotProducts(30)
hotProducts = hotResult as Product[]
} catch (hotError) {
console.error('获取热销商品失败,使用空列表:', hotError)
hotProducts = []
}
const hotList: Array<HotSearchItemType> = []
const limit1 = hotProducts.length < 10 ? hotProducts.length : 10
for (let i: number = 0; i < limit1; i++) {
const p = hotProducts[i]
const item: HotSearchItemType = {
keyword: p.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]
const saleCount = p.sale_count
const item: GuessItemType = {
id: p.id ?? '',
name: p.name ?? '',
price: p.base_price ?? 0,
image: p.main_image_url ?? '/static/default.jpg',
sales: saleCount != null ? saleCount : 0,
merchant_id: p.merchant_id ?? ''
}
allItems.push(item)
}
allGuessItems.value = allItems
refreshGuessListItems()
} catch (e) {
console.error('Load data failed', e)
// 不再显示错误页面,允许使用空数据
isError.value = false
}
}
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
}
console.log('Search execution started for keyword:', keyword)
let sortBy = 'sales'
let ascending = false
if (activeSort.value === 'price') {
sortBy = 'price'
ascending = priceSortAsc.value
} else if (activeSort.value === 'default') {
sortBy = 'default'
}
try {
console.log('Calling searchProducts with params:', keyword, currentPage.value, sortBy, ascending)
const prodResp = await supabaseService.searchProducts(keyword, currentPage.value, 20, sortBy, ascending)
console.log('searchProducts response received:', prodResp.data != null ? prodResp.data.length : 0, 'items')
let shopList: Array<ShopResultType> = []
if (currentPage.value === 1 && activeSort.value === 'default') {
const shopResp = await supabaseService.searchShops(keyword)
if (shopResp.data != null && shopResp.data.length > 0) {
for (let i: number = 0; i < shopResp.data.length; i++) {
const s = shopResp.data[i]
const shopItem: ShopResultType = {
id: s.id ?? '',
name: s.shop_name ?? '',
logo: s.shop_logo ?? '/static/shop_logo_default.png',
productCount: s.product_count ?? 0
}
shopList.push(shopItem)
}
}
}
searchShopResults.value = shopList
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 Product
let tag = ''
const tagsRaw = p.tags
if (tagsRaw != null) {
try {
const tagsStr = p.tags
if (tagsStr != null) {
const tags = JSON.parse(tagsStr as string)
if (Array.isArray(tags) && tags.length > 0) {
const firstTag = tags[0]
tag = firstTag != null ? (firstTag as string) : ''
}
}
} catch(e) {}
}
const searchItem: SearchResultType = {
id: p.id ?? '',
name: p.name ?? '',
image: p.main_image_url ?? '/static/default.jpg',
price: p.base_price ?? 0,
specification: p.specification ?? '标准规格',
tag: tag,
sales: p.sale_count ?? 0,
merchant_id: p.merchant_id ?? ''
}
resultList.push(searchItem)
}
searchResults.value = resultList
hasMore.value = prodResp.hasmore
} catch(e) {
console.error('Search failed detailed error:', 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) => {
try {
let val = ''
// 处理 input 事件的不同事件对象格式
if (e != null) {
// UTSJSONObject 格式 (e.detail.value)
if (e instanceof UTSJSONObject) {
const eObj = e as UTSJSONObject
const detailObj = eObj.get('detail')
if (detailObj != null && detailObj instanceof UTSJSONObject) {
const detail = detailObj as UTSJSONObject
const v = detail.get('value')
val = v != null ? (v as string) : ''
}
} else {
// 尝试转换为 UTSJSONObject
const eObj = JSON.parse(JSON.stringify(e)) as UTSJSONObject
const detailObj = eObj.get('detail')
if (detailObj != null) {
const detail = detailObj as UTSJSONObject
const v = detail.get('value')
val = v != null ? (v as string) : ''
} else {
const v = eObj.get('value')
val = v != null ? (v as string) : ''
}
}
}
searchKeyword.value = val
if (val == '') {
showResults.value = false
searchSuggestions.value = []
return
}
if (suggestTimer > 0) clearTimeout(suggestTimer)
suggestTimer = setTimeout(() => {
fetchSuggestions(val)
}, 300)
} catch (err) {
console.error('onInput error:', err)
}
}
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 Product
let tag = ''
const tagsRaw = p.tags
if (tagsRaw != null) {
try {
const tagsStr = p.tags
if (tagsStr != null) {
const tags = JSON.parse(tagsStr as string)
if (Array.isArray(tags) && tags.length > 0) {
const firstTag = tags[0]
tag = firstTag != null ? (firstTag as string) : ''
}
}
} catch(e) {}
}
const searchItem: SearchResultType = {
id: p.id ?? '',
name: p.name ?? '',
image: p.main_image_url ?? '/static/default.jpg',
price: p.base_price ?? 0,
specification: p.specification ?? '标准规格',
tag: tag,
sales: p.sale_count ?? 0,
merchant_id: p.merchant_id ?? ''
}
searchResults.value.push(searchItem)
}
hasMore.value = response.hasmore
} catch(e) {
console.error('Load more failed', e)
hasMore.value = false
} finally {
loading.value = false
}
}
onReachBottom(() => {
if (showResults.value) {
loadMore()
}
})
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 = async (product: SearchResultType | GuessItemType) => {
uni.showLoading({ title: '检查商品...' })
try {
// 统一转换为 UTSJSONObject 访问属性
const prodObj = JSON.parse(JSON.stringify(product)) as UTSJSONObject
const productId = prodObj.getString('id') ?? ''
const merchantId = prodObj.getString('merchant_id') ?? ''
// 检查商品是否有SKU
const skus = await supabaseService.getProductSkus(productId)
uni.hideLoading()
if (skus.length > 0) {
// 有规格,提示并跳转到商品详情页选择规格
uni.showToast({ title: '请选择规格', icon: 'none' })
setTimeout(() => {
uni.navigateTo({
url: '/pages/mall/consumer/product-detail?id=' + productId
})
}, 500)
} else {
// 无规格,直接加入购物车
uni.showLoading({ title: '添加中...' })
const success = await supabaseService.addToCart(productId, 1, '', merchantId)
uni.hideLoading()
if (success) {
uni.showToast({ title: '已添加到购物车', icon: 'success' })
} else {
uni.showToast({ title: '添加失败,请先登录', icon: 'none' })
}
}
} catch (e) {
console.error('添加到购物车异常', e)
uni.hideLoading()
uni.showToast({ title: '操作异常', icon: 'none' })
}
}
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/main/index'
})
}
}
}
</script>
<style>
.search-page {
width: 100%;
flex: 1; /* Fixed 100vh */
background-color: #f5f5f5;
display: flex;
flex-direction: column;
min-height: 100vh; /* 确保背景色覆盖全屏 */
}
/* 店铺搜索结果 */
.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 */
flex-shrink: 0; /* 禁止头部被压缩 */
}
.search-bar-container {
display: flex;
flex-direction: row; /* UVUE 必须显式设置 row */
align-items: center;
padding: 10px 16px;
box-sizing: border-box;
}
.back-btn {
padding: 4px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
width: 32px; /* 固定宽度防止压缩 */
height: 32px;
margin-right: 12px;
flex-shrink: 0;
}
.back-icon {
font-size: 20px;
color: #333;
}
.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 {
display: flex;
flex-direction: column;
background: #fff;
border-radius: 8px;
overflow: hidden;
width: 48%; /* 手机端 2列 */
margin-bottom: 12px;
}
/* 猜测列表响应式,参照 index.uvue 的 hot-products */
@media screen and (min-width: 769px) {
.guess-item {
width: 32%; /* 平板 3列 */
}
}
@media screen and (min-width: 1025px) {
.guess-item {
width: 23%; /* 电脑 4列 */
}
}
.guess-img {
width: 100%;
height: 170px;
border-radius: 8px;
margin-bottom: 8px;
background: #f5f5f5;
object-fit: cover;
}
.guess-name {
font-size: 13px;
color: #333;
margin-bottom: 5px;
line-height: 1.4;
height: 36px;
overflow: hidden;
text-overflow: ellipsis;
padding: 0 8px;
}
.guess-bottom {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 0 8px 8px;
}
.guess-price {
font-size: 15px;
color: #ff5000;
font-weight: bold;
}
.guess-add-btn {
width: 24px;
height: 24px;
background-color: #ff5000;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.guess-add-icon {
color: #fff;
font-size: 16px;
font-weight: bold;
}
/* 搜索建议列表 */
.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;
}
.results-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 10px 12px;
background-color: #fff;
margin-bottom: 2px;
}
.results-title {
font-size: 15px;
font-weight: bold;
color: #333;
}
.filter-tabs {
display: flex;
flex-direction: row;
align-items: center;
flex-wrap: wrap;
justify-content: flex-start;
}
.filter-tab {
font-size: 13px;
color: #666;
padding: 8px 12px;
border-radius: 20px;
border: 1px solid #e0e0e0;
transition: all 0.2s ease;
white-space: nowrap;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
margin-left: 8px;
}
.filter-tab.active {
background: #ff5000;
color: white;
border-color: #ff5000;
}
.filter-tab:hover {
background: #f5f5f5;
}
/* 搜索结果列表 */
.search-results {
padding-bottom: 20px;
width: 100%;
display: flex;
flex-direction: column;
align-items: flex-start; /* 核心修复:确保内容不居中缩进 */
}
.results-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 10px 12px;
background-color: #fff;
margin-bottom: 2px;
width: 100%; /* 核心修复:确保标题栏撑满宽度 */
box-sizing: border-box;
}
.results-list {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: flex-start;
padding: 10px;
width: 100%;
box-sizing: border-box;
margin-top: 5px;
background-color: #fff; /* 为列表添加背景色 */
}
.result-item {
display: flex;
flex-direction: column;
background: #fff;
border-radius: 8px;
overflow: hidden;
/* width: calc(50% - 20px); 手机端一行2个 */
width: 48%;
margin-bottom: 12px;
margin-right: 2%;
border: 1px solid #f0f0f0; /* 添加微弱边框增加层次感 */
}
/* 电脑端响应式覆盖 - 强制拉伸宽度 */
@media screen and (min-width: 1025px) {
.main-content {
width: 1200px; /* 改为固定宽度或更大百分比 */
max-width: 95%;
margin: 0 auto;
padding: 20px 32px;
}
.result-item {
width: 23%; /* 4列布局 */
margin-right: 2%;
}
}
/* 大桌面端 (1400px以上) */
@media screen and (min-width: 1400px) {
.result-item {
width: 23%; /* 保持一行4个或者根据需要调整为 18% (一行5个) */
}
}
.product-image {
width: 100%;
height: 170px; /* 与主页一致 */
/* aspect-ratio: 1 / 1; */
object-fit: cover;
background-color: #f5f5f5;
border-radius: 8px;
margin-bottom: 8px;
}
.product-name {
font-size: 13px;
color: #333;
margin-bottom: 5px;
line-height: 1.4;
height: 36px;
overflow: hidden;
text-overflow: ellipsis;
padding: 0 8px;
}
.product-bottom {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 0 8px 8px;
}
.product-price {
font-size: 15px;
color: #ff5000;
font-weight: bold;
}
.product-add-btn {
width: 24px;
height: 24px;
background-color: #ff5000;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.add-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;
}
.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>