1850 lines
44 KiB
Plaintext
1850 lines
44 KiB
Plaintext
<template>
|
||
<view class="search-page">
|
||
<view class="search-navbar" :style="navbarStyle">
|
||
<view class="back-btn" @click="goBack">
|
||
<text class="back-icon"><</text>
|
||
</view>
|
||
|
||
<view class="jd-search-box">
|
||
<input
|
||
class="jd-search-input"
|
||
type="text"
|
||
:value="searchKeyword"
|
||
@input="onInput"
|
||
@confirm="onSearch"
|
||
:placeholder="(placeholderKeyword != null && placeholderKeyword !== '' && searchKeyword === '') ? placeholderKeyword : '请输入商品名称、店铺'"
|
||
placeholder-class="jd-placeholder"
|
||
:focus="autoFocus"
|
||
confirm-type="search"
|
||
/>
|
||
|
||
<view v-if="searchKeyword != '' && searchKeyword != null" class="clear-wrap" @click.stop="clearSearch">
|
||
<text class="clear-icon">×</text>
|
||
</view>
|
||
|
||
<view class="camera-wrap" @tap.stop="handleCameraSearch">
|
||
<text class="camera-icon">拍</text>
|
||
</view>
|
||
|
||
<view class="search-submit-btn" @tap.stop="onSearch">
|
||
<text class="search-submit-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 鏀寔瀹夊崜绔粴鍔?-->
|
||
<scroll-view
|
||
v-else
|
||
class="main-content"
|
||
direction="vertical"
|
||
:show-scrollbar="false"
|
||
:lower-threshold="120"
|
||
@scrolltolower="handleMainScrollToLower"
|
||
>
|
||
<!-- 初始状态 -->
|
||
<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">
|
||
<view v-if="!isEditMode" @click="toggleHistoryEdit">
|
||
<text class="edit-text">编辑</text>
|
||
</view>
|
||
<view v-else style="display:flex;flex-direction:row;align-items:center;">
|
||
<text class="all-clear" @click="clearHistory" style="margin-right:12px;color:#ff4d1a">全部删除</text>
|
||
<text class="done" @click="toggleHistoryEdit">完成</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view class="history-tags">
|
||
<view
|
||
v-for="(item, index) in visibleHistory"
|
||
:key="index"
|
||
class="history-tag"
|
||
@click="handleHistoryTagClick(item)"
|
||
>
|
||
<text class="history-text">{{ item }}</text>
|
||
<view v-if="isEditMode" class="delete-tag-btn" @click.stop="deleteHistoryItemByKeyword(item)">
|
||
<text class="delete-icon">×</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view v-if="searchHistory.length > MAX_COLLAPSED_COUNT" style="padding:8px 4px;">
|
||
<text class="more-toggle" @click="toggleHistoryExpanded">{{ historyExpanded ? '收起' : '更多 v' }}</text>
|
||
</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">
|
||
<GuessYouLike
|
||
title="猜你喜欢"
|
||
:pageSize="8"
|
||
:loadMoreKey="guessLoadMoreKey"
|
||
@productClick="goToProductDetail"
|
||
/>
|
||
</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 v-if="product.cardTags.length > 0" class="card-tags-row">
|
||
<text v-for="(tag, index) in product.cardTags" :key="product.id + '-result-tag-' + index" class="card-tag">{{ tag }}</text>
|
||
</view>
|
||
<text class="product-name" :lines="2">{{ product.name }}</text>
|
||
<text v-if="product.highlight !== ''" class="card-highlight">{{ product.highlight }}</text>
|
||
<view v-if="product.serviceTags.length > 0" class="service-tags-row">
|
||
<text v-for="(tag, index) in product.serviceTags" :key="product.id + '-result-service-' + index" class="service-tag">{{ tag }}</text>
|
||
</view>
|
||
<view class="product-bottom">
|
||
<view class="price-stack">
|
||
<text class="product-price">¥{{ product.price }}</text>
|
||
<text v-if="product.marketPrice !== ''" class="market-price">¥{{ product.marketPrice }}</text>
|
||
</view>
|
||
<view class="product-add-btn" @click.stop="addToCart(product)">
|
||
<text class="add-icon">+</text>
|
||
</view>
|
||
</view>
|
||
<text v-if="product.salesText !== ''" class="card-sales-text">{{ product.salesText }}</text>
|
||
</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 v-if="searchResults.length > 0" class="guess-you-like" style="margin-top: 16rpx;">
|
||
<GuessYouLike
|
||
title="猜你喜欢"
|
||
:pageSize="8"
|
||
:excludeProductIds="searchResultProductIds"
|
||
:loadMoreKey="guessLoadMoreKey"
|
||
@productClick="goToProductDetail"
|
||
/>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 底部安全区域 -->
|
||
<view class="safe-area"></view>
|
||
</scroll-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'
|
||
import { goToLogin } from '@/utils/utils.uts'
|
||
import GuessYouLike from '@/components/mall/GuessYouLike/GuessYouLike.uvue'
|
||
|
||
// 状态定义
|
||
const statusBarHeight = ref(0)
|
||
const scrollHeight = ref(0)
|
||
const searchKeyword = ref('')
|
||
const placeholderKeyword = ref('')
|
||
const navRightReserve = ref(0)
|
||
const navBarTop = ref(0)
|
||
const navBarHeight = ref(44)
|
||
const showResults = ref(false)
|
||
const loading = ref(false)
|
||
const hasMore = ref(true)
|
||
const isError = ref(false)
|
||
const autoFocus = ref(true)
|
||
const guessLoadMoreKey = ref<number>(0)
|
||
|
||
const navbarStyle = computed(() => {
|
||
const top = navBarTop.value > 0 ? navBarTop.value : statusBarHeight.value
|
||
const h = navBarHeight.value > 0 ? navBarHeight.value : 44
|
||
const pr = navRightReserve.value > 0 ? navRightReserve.value : 12
|
||
return `padding-top:${top}px;height:${top + h + 8}px;padding-right:${pr}px;padding-left:12px;`
|
||
})
|
||
|
||
const handleCameraSearch = () => {
|
||
uni.showToast({ title: '相机搜索暂未开放', icon: 'none' })
|
||
}
|
||
|
||
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
|
||
marketPrice: string
|
||
image: string
|
||
sales: number
|
||
salesText: string
|
||
highlight: string
|
||
cardTags: string[]
|
||
serviceTags: string[]
|
||
merchant_id: string
|
||
}
|
||
|
||
type SearchResultType = {
|
||
id: string
|
||
name: string
|
||
image: string
|
||
price: number
|
||
marketPrice: string
|
||
specification: string
|
||
tag: string
|
||
sales: number
|
||
salesText: string
|
||
highlight: string
|
||
cardTags: string[]
|
||
serviceTags: string[]
|
||
merchant_id: string
|
||
}
|
||
|
||
const getCardTitle = (product: Product): string => {
|
||
if (product.short_title != null && product.short_title !== '') return product.short_title
|
||
if (product.name != null && product.name !== '') return product.name
|
||
return product.id ?? ''
|
||
}
|
||
|
||
const getCardHighlight = (product: Product): string => {
|
||
if (product.selling_points != null && product.selling_points.length > 0 && product.selling_points[0] !== '') {
|
||
return product.selling_points[0]
|
||
}
|
||
if (product.subtitle != null && product.subtitle !== '') return product.subtitle
|
||
return ''
|
||
}
|
||
|
||
const getCardTags = (product: Product): string[] => {
|
||
if (product.card_tags != null && product.card_tags.length > 0) return product.card_tags.slice(0, 2)
|
||
const tags: string[] = []
|
||
if (product.is_hot === true) tags.push('热卖')
|
||
if (product.is_new === true && tags.length < 2) tags.push('新品')
|
||
if (product.is_featured === true && tags.length < 2) tags.push('精选')
|
||
return tags
|
||
}
|
||
|
||
const getServiceTags = (product: Product): string[] => {
|
||
if (product.service_tags != null && product.service_tags.length > 0) return product.service_tags.slice(0, 3)
|
||
return [] as string[]
|
||
}
|
||
|
||
const formatSalesText = (product: Product): string => {
|
||
if (product.display_sales_text != null && product.display_sales_text !== '') return product.display_sales_text
|
||
const sales = product.sale_count ?? 0
|
||
if (sales >= 100000) return '已售10万+'
|
||
if (sales >= 10000) return '已售' + (sales / 10000).toFixed(1) + '万件'
|
||
if (sales > 0) return '已售' + sales.toString() + '件'
|
||
return ''
|
||
}
|
||
|
||
const formatMarketPriceText = (product: Product): string => {
|
||
const marketPrice = product.market_price ?? product.original_price ?? 0
|
||
const salePrice = product.base_price ?? product.price ?? 0
|
||
if (marketPrice > salePrice && marketPrice > 0) return parseFloat(marketPrice.toString()).toFixed(2)
|
||
return ''
|
||
}
|
||
|
||
type ShopResultType = {
|
||
id: string
|
||
name: string
|
||
logo: string
|
||
productCount: number
|
||
}
|
||
|
||
const getCurrentPageOptions = (): UTSJSONObject => {
|
||
const pages = getCurrentPages()
|
||
if (pages.length == 0) {
|
||
return new UTSJSONObject()
|
||
}
|
||
const currentPage = pages[pages.length - 1]
|
||
const pageOptions = currentPage.options
|
||
if (pageOptions == null) {
|
||
return new UTSJSONObject()
|
||
}
|
||
return pageOptions as UTSJSONObject
|
||
}
|
||
|
||
const searchHistory = ref<Array<string>>([])
|
||
const hotSearchList = ref<Array<HotSearchItemType>>([])
|
||
const guessList = ref<Array<GuessItemType>>([])
|
||
const allGuessItems = ref<Array<GuessItemType>>([])
|
||
|
||
const DEFAULT_HOT_KEYWORDS: Array<string> = [
|
||
'澶х枂neo2',
|
||
'iPhone 15 Pro',
|
||
'Nike Air Max 270',
|
||
'厨具',
|
||
'老干妈',
|
||
'钢化膜',
|
||
'手机壳',
|
||
'零食坚果',
|
||
'新鲜水果',
|
||
'液态硅胶壳',
|
||
'充电宝',
|
||
'蓝牙耳机'
|
||
]
|
||
|
||
// 推荐商品区
|
||
const recommendList = ref<Array<GuessItemType>>([])
|
||
const recommendPool = ref<Array<GuessItemType>>([])
|
||
const recommendPage = ref(0)
|
||
const recommendInitialSize = ref(8)
|
||
const recommendAppendSize = ref(20)
|
||
const loadingRecommend = ref(false)
|
||
const searchResults = ref<Array<SearchResultType>>([])
|
||
const searchShopResults = ref<Array<ShopResultType>>([])
|
||
const searchResultProductIds = computed((): Array<string> => {
|
||
const ids: Array<string> = []
|
||
for (let i = 0; i < searchResults.value.length; i++) {
|
||
const id = searchResults.value[i].id
|
||
if (id !== '' && ids.indexOf(id) < 0) {
|
||
ids.push(id)
|
||
}
|
||
}
|
||
return ids
|
||
})
|
||
|
||
const SEARCH_HISTORY_KEY = 'consumer_search_history'
|
||
|
||
const loadSearchHistory = () => {
|
||
try {
|
||
const historyRaw = uni.getStorageSync(SEARCH_HISTORY_KEY)
|
||
if (historyRaw != null && historyRaw !== '') {
|
||
const parsed = JSON.parse(historyRaw as string)
|
||
if (Array.isArray(parsed)) searchHistory.value = parsed as string[]
|
||
}
|
||
} catch (e) {
|
||
searchHistory.value = []
|
||
}
|
||
}
|
||
|
||
const saveSearchHistory = () => {
|
||
try {
|
||
uni.setStorageSync(SEARCH_HISTORY_KEY, JSON.stringify(searchHistory.value))
|
||
} catch (e) {
|
||
// ignore
|
||
}
|
||
}
|
||
|
||
const addToHistory = (keyword: string) => {
|
||
try {
|
||
if (keyword == null) return
|
||
const kw = keyword.trim()
|
||
if (kw === '') return
|
||
const index = searchHistory.value.indexOf(kw)
|
||
if (index > -1) searchHistory.value.splice(index, 1)
|
||
searchHistory.value.unshift(kw)
|
||
if (searchHistory.value.length > 50) searchHistory.value.length = 50
|
||
saveSearchHistory()
|
||
} catch (e) {
|
||
// ignore
|
||
}
|
||
}
|
||
|
||
const isEditMode = ref(false)
|
||
const historyExpanded = ref(false)
|
||
const MAX_COLLAPSED_COUNT = 8
|
||
const MAX_EXPANDED_COUNT = 24
|
||
|
||
const clearHistory = () => {
|
||
uni.showModal({
|
||
title: '提示',
|
||
content: '确定清空搜索历史吗?',
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
searchHistory.value = []
|
||
try { uni.removeStorageSync(SEARCH_HISTORY_KEY) } catch (e) {}
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
const deleteHistoryItem = (index: number) => {
|
||
if (index >= 0 && index < searchHistory.value.length) {
|
||
searchHistory.value.splice(index, 1)
|
||
saveSearchHistory()
|
||
}
|
||
}
|
||
|
||
const toggleHistoryEdit = () => { isEditMode.value = !isEditMode.value }
|
||
const toggleHistoryExpanded = () => { historyExpanded.value = !historyExpanded.value }
|
||
|
||
const visibleHistory = computed(() => {
|
||
if (historyExpanded.value) {
|
||
const maxLen = searchHistory.value.length < MAX_EXPANDED_COUNT ? searchHistory.value.length : MAX_EXPANDED_COUNT
|
||
return searchHistory.value.slice(0, maxLen)
|
||
}
|
||
return searchHistory.value.slice(0, MAX_COLLAPSED_COUNT)
|
||
})
|
||
|
||
const deleteHistoryItemByKeyword = (keyword: string) => {
|
||
try {
|
||
const idx = searchHistory.value.indexOf(keyword)
|
||
if (idx > -1) {
|
||
searchHistory.value.splice(idx, 1)
|
||
saveSearchHistory()
|
||
}
|
||
} catch (e) {}
|
||
}
|
||
|
||
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 < 12 ? hotProducts.length : 12
|
||
for (let i: number = 0; i < limit1; i++) {
|
||
const p = hotProducts[i]
|
||
hotList.push({ keyword: p.name ?? '', hot: i < 3 })
|
||
}
|
||
|
||
if (hotList.length < 12) {
|
||
for (let i: number = 0; i < DEFAULT_HOT_KEYWORDS.length; i++) {
|
||
let found = false
|
||
for (let j: number = 0; j < hotList.length; j++) {
|
||
if (hotList[j].keyword === DEFAULT_HOT_KEYWORDS[i]) {
|
||
found = true
|
||
break
|
||
}
|
||
}
|
||
if (found === false) {
|
||
hotList.push({ keyword: DEFAULT_HOT_KEYWORDS[i], hot: i < 3 })
|
||
}
|
||
if (hotList.length >= 12) break
|
||
}
|
||
}
|
||
|
||
if (searchHistory.value.length > 0) {
|
||
const historyFirst: Array<HotSearchItemType> = []
|
||
const histSlice = searchHistory.value.slice(0, 5)
|
||
for (let i: number = 0; i < histSlice.length; i++) {
|
||
historyFirst.push({ keyword: histSlice[i], hot: false })
|
||
}
|
||
const merged = historyFirst.concat(hotList)
|
||
const seen = new Set<string>()
|
||
const dedup: Array<HotSearchItemType> = []
|
||
for (let i: number = 0; i < merged.length; i++) {
|
||
const it = merged[i]
|
||
if (seen.has(it.keyword) === false && it.keyword != null && it.keyword !== '') {
|
||
dedup.push(it)
|
||
seen.add(it.keyword)
|
||
}
|
||
if (dedup.length >= 12) break
|
||
}
|
||
hotSearchList.value = dedup
|
||
} else {
|
||
hotSearchList.value = hotList
|
||
}
|
||
|
||
const pool: Array<GuessItemType> = []
|
||
for (let i: number = 0; i < hotProducts.length; i++) {
|
||
const p = hotProducts[i]
|
||
pool.push({
|
||
id: p.id ?? '',
|
||
name: getCardTitle(p),
|
||
price: p.base_price ?? 0,
|
||
marketPrice: formatMarketPriceText(p),
|
||
image: p.main_image_url ?? '/static/default.jpg',
|
||
sales: p.sale_count ?? 0,
|
||
salesText: formatSalesText(p),
|
||
highlight: getCardHighlight(p),
|
||
cardTags: getCardTags(p),
|
||
serviceTags: getServiceTags(p),
|
||
merchant_id: p.merchant_id ?? ''
|
||
})
|
||
}
|
||
recommendPool.value = pool
|
||
recommendList.value = recommendPool.value.slice(0, recommendInitialSize.value)
|
||
recommendPage.value = 1
|
||
allGuessItems.value = pool
|
||
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: getCardTitle(p),
|
||
image: p.main_image_url ?? '/static/default.jpg',
|
||
price: p.base_price ?? 0,
|
||
marketPrice: formatMarketPriceText(p),
|
||
specification: p.specification ?? '标准规格',
|
||
tag: tag,
|
||
sales: p.sale_count ?? 0,
|
||
salesText: formatSalesText(p),
|
||
highlight: getCardHighlight(p),
|
||
cardTags: getCardTags(p),
|
||
serviceTags: getServiceTags(p),
|
||
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)
|
||
|
||
navBarTop.value = statusBarHeight.value
|
||
navBarHeight.value = 44
|
||
navRightReserve.value = 0
|
||
// #ifdef MP-WEIXIN
|
||
try {
|
||
const menuBtn = uni.getMenuButtonBoundingClientRect()
|
||
if (menuBtn != null && menuBtn.top != null && menuBtn.top > 0) {
|
||
navBarTop.value = menuBtn.top
|
||
navBarHeight.value = menuBtn.height
|
||
navRightReserve.value = (systemInfo.windowWidth - menuBtn.left) + 8
|
||
}
|
||
} catch (e) {
|
||
navBarTop.value = statusBarHeight.value
|
||
navBarHeight.value = 44
|
||
navRightReserve.value = 0
|
||
}
|
||
// #endif
|
||
|
||
loadData()
|
||
|
||
loadSearchHistory()
|
||
|
||
const options = getCurrentPageOptions()
|
||
const kwRaw = options.getString('keyword')
|
||
const src = options.getString('source')
|
||
if (kwRaw != null && kwRaw !== '') {
|
||
const decoded = decodeURIComponent(kwRaw)
|
||
const keyword = decoded != null ? decoded : kwRaw
|
||
if (src === 'placeholder') {
|
||
placeholderKeyword.value = keyword
|
||
searchKeyword.value = ''
|
||
} else {
|
||
searchKeyword.value = keyword
|
||
}
|
||
|
||
const typeVal = options.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: UniInputEvent) => {
|
||
try {
|
||
const val = e.detail.value 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 = () => {
|
||
const userInput = searchKeyword.value.trim()
|
||
let effective = ''
|
||
if (userInput !== '') {
|
||
effective = userInput
|
||
} else if (placeholderKeyword.value != null && placeholderKeyword.value !== '') {
|
||
effective = placeholderKeyword.value
|
||
}
|
||
if (effective === '') return
|
||
addToHistory(effective)
|
||
// 如果搜索词来自 placeholder,保持输入框为空但执行搜索
|
||
if (userInput === '') {
|
||
// 保持 searchKeyword 为空,但使用 effective 搜索
|
||
searchKeyword.value = ''
|
||
}
|
||
// 临时将 searchKeyword 设置为 effective,供 performSearch 使用
|
||
const prev = searchKeyword.value
|
||
searchKeyword.value = effective
|
||
performSearch()
|
||
// 如果用户没有手动输入,则恢复为空
|
||
if (userInput === '') searchKeyword.value = prev
|
||
}
|
||
|
||
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 handleHistoryTagClick = (keyword: string): void => {
|
||
if (isEditMode.value) {
|
||
return
|
||
}
|
||
searchFromHistory(keyword)
|
||
}
|
||
|
||
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: getCardTitle(p),
|
||
image: p.main_image_url ?? '/static/default.jpg',
|
||
price: p.base_price ?? 0,
|
||
marketPrice: formatMarketPriceText(p),
|
||
specification: p.specification ?? '标准规格',
|
||
tag: tag,
|
||
sales: p.sale_count ?? 0,
|
||
salesText: formatSalesText(p),
|
||
highlight: getCardHighlight(p),
|
||
cardTags: getCardTags(p),
|
||
serviceTags: getServiceTags(p),
|
||
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
|
||
}
|
||
}
|
||
|
||
function handleReachBottom(): void {
|
||
if (showResults.value === true) {
|
||
if (hasMore.value) {
|
||
loadMore()
|
||
return
|
||
}
|
||
guessLoadMoreKey.value = guessLoadMoreKey.value + 1
|
||
return
|
||
}
|
||
guessLoadMoreKey.value = guessLoadMoreKey.value + 1
|
||
}
|
||
|
||
function handleMainScrollToLower(): void {
|
||
handleReachBottom()
|
||
}
|
||
|
||
onReachBottom(() => {
|
||
handleReachBottom()
|
||
})
|
||
|
||
const viewShopDetail = (shop: ShopResultType) => {
|
||
uni.navigateTo({
|
||
url: `/pages/mall/consumer/shop-detail?id=${shop.id}`
|
||
})
|
||
}
|
||
|
||
const addToCart = async (product: SearchResultType | GuessItemType) => {
|
||
const userId = supabaseService.getCurrentUserId()
|
||
if (userId == null || userId === '') {
|
||
goToLogin('/pages/mall/consumer/search')
|
||
return
|
||
}
|
||
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 as string[])[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;
|
||
background-color: #f7f7f7;
|
||
display: flex;
|
||
flex-direction: column;
|
||
min-height: 100%;
|
||
}
|
||
|
||
/* 搴楅摵鎼滅储缁撴灉 */
|
||
.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-navbar {
|
||
position: relative;
|
||
z-index: 20;
|
||
background: #ffffff;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
box-sizing: border-box;
|
||
border-bottom: 1rpx solid #f2f2f2;
|
||
}
|
||
|
||
.back-btn {
|
||
flex: 0 0 56rpx;
|
||
height: 64rpx;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: flex-start;
|
||
margin-right: 8rpx;
|
||
}
|
||
|
||
.back-icon {
|
||
font-size: 52rpx;
|
||
color: #222222;
|
||
line-height: 1;
|
||
}
|
||
|
||
.jd-search-box {
|
||
flex: 1;
|
||
height: 64rpx;
|
||
border: 2rpx solid #e1251b;
|
||
border-radius: 12rpx;
|
||
background: #ffffff;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
overflow: hidden;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.jd-search-input {
|
||
flex: 1;
|
||
height: 64rpx;
|
||
line-height: 64rpx;
|
||
padding-left: 20rpx;
|
||
padding-right: 12rpx;
|
||
font-size: 28rpx;
|
||
color: #222222;
|
||
box-sizing: border-box;
|
||
background-color: transparent;
|
||
}
|
||
|
||
.jd-placeholder {
|
||
color: #b8b8b8;
|
||
font-size: 28rpx;
|
||
}
|
||
|
||
.clear-wrap {
|
||
flex: 0 0 auto;
|
||
height: 64rpx;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 0 8rpx;
|
||
}
|
||
|
||
.clear-icon {
|
||
font-size: 28rpx;
|
||
color: #999;
|
||
}
|
||
|
||
.camera-wrap {
|
||
flex: 0 0 64rpx;
|
||
height: 64rpx;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.camera-icon {
|
||
font-size: 34rpx;
|
||
color: #999999;
|
||
}
|
||
|
||
.search-submit-btn {
|
||
flex: 0 0 104rpx;
|
||
height: 64rpx;
|
||
background: #e1251b;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.search-submit-text {
|
||
color: #ffffff;
|
||
font-size: 30rpx;
|
||
font-weight: 600;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.main-content {
|
||
flex: 1;
|
||
padding: 24rpx;
|
||
box-sizing: border-box;
|
||
height: 0;
|
||
background-color: #f7f7f7;
|
||
}
|
||
|
||
.section-header {
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 16rpx;
|
||
margin-top: 16rpx;
|
||
width: 100%;
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 30rpx;
|
||
font-weight: bold;
|
||
color: #222222;
|
||
flex: 1;
|
||
}
|
||
|
||
.header-right {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.edit-text {
|
||
font-size: 26rpx;
|
||
color: #999;
|
||
}
|
||
|
||
.all-clear {
|
||
font-size: 26rpx;
|
||
color: #ff4d1a;
|
||
margin-right: 16rpx;
|
||
}
|
||
|
||
.done {
|
||
font-size: 26rpx;
|
||
color: #222222;
|
||
}
|
||
|
||
.more-toggle {
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
}
|
||
|
||
.search-history {
|
||
margin-bottom: 24rpx;
|
||
background-color: #ffffff;
|
||
border-radius: 20rpx;
|
||
padding: 20rpx 24rpx;
|
||
}
|
||
|
||
.history-tags {
|
||
display: flex;
|
||
flex-direction: row;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
}
|
||
|
||
.history-tag {
|
||
background-color: #f5f5f7;
|
||
padding: 10rpx 24rpx;
|
||
border-radius: 999rpx;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
flex-shrink: 0;
|
||
margin-right: 16rpx;
|
||
margin-bottom: 16rpx;
|
||
height: 56rpx;
|
||
}
|
||
|
||
.history-text {
|
||
font-size: 26rpx;
|
||
color: #666;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
margin-right: 8rpx;
|
||
max-width: 200rpx;
|
||
}
|
||
|
||
.delete-tag-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 28rpx;
|
||
height: 28rpx;
|
||
border-radius: 14rpx;
|
||
background-color: #f0f0f0;
|
||
}
|
||
|
||
.delete-icon {
|
||
font-size: 20rpx;
|
||
color: #999;
|
||
}
|
||
|
||
.hot-search {
|
||
margin-bottom: 24rpx;
|
||
background-color: #ffffff;
|
||
border-radius: 20rpx;
|
||
padding: 20rpx 24rpx;
|
||
}
|
||
|
||
.hot-tags {
|
||
display: flex;
|
||
flex-direction: row;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.hot-tag {
|
||
background-color: #f5f5f7;
|
||
padding: 10rpx 24rpx;
|
||
border-radius: 999rpx;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
flex-shrink: 0;
|
||
margin-right: 16rpx;
|
||
margin-bottom: 16rpx;
|
||
height: 56rpx;
|
||
}
|
||
|
||
.hot-tag.hot {
|
||
background-color: #fff0f0;
|
||
}
|
||
|
||
.hot-rank {
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
font-weight: bold;
|
||
margin-right: 8rpx;
|
||
}
|
||
|
||
.hot-rank.top-three {
|
||
color: #ff4d1a;
|
||
}
|
||
|
||
.hot-text {
|
||
font-size: 26rpx;
|
||
color: #222222;
|
||
}
|
||
|
||
.hot-icon {
|
||
font-size: 22rpx;
|
||
margin-left: 6rpx;
|
||
}
|
||
|
||
/* 鐚滀綘闇€瑕?*/
|
||
.guess-you-like {
|
||
margin-bottom: 24rpx;
|
||
background-color: #ffffff;
|
||
border-radius: 20rpx;
|
||
padding: 20rpx 24rpx;
|
||
}
|
||
|
||
.title-with-icon {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
}
|
||
|
||
.section-icon {
|
||
font-size: 30rpx;
|
||
margin-right: 8rpx;
|
||
}
|
||
|
||
.refresh-btn {
|
||
font-size: 24rpx;
|
||
color: #ff4d1a;
|
||
}
|
||
|
||
.guess-grid {
|
||
display: flex;
|
||
flex-direction: row;
|
||
flex-wrap: wrap;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.guess-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
background: #ffffff;
|
||
border-radius: 20rpx;
|
||
overflow: hidden;
|
||
width: 48%;
|
||
margin-bottom: 20rpx;
|
||
border: 1rpx solid #f0f0f0;
|
||
}
|
||
|
||
@media screen and (min-width: 769px) {
|
||
.guess-item {
|
||
width: 32%;
|
||
}
|
||
}
|
||
|
||
@media screen and (min-width: 1025px) {
|
||
.guess-item {
|
||
width: 23%;
|
||
}
|
||
}
|
||
|
||
.guess-img {
|
||
width: 100%;
|
||
height: 280rpx;
|
||
border-radius: 16rpx;
|
||
margin-bottom: 12rpx;
|
||
background: #f5f5f7;
|
||
object-fit: cover;
|
||
}
|
||
|
||
.guess-name {
|
||
font-size: 26rpx;
|
||
color: #222222;
|
||
margin-bottom: 8rpx;
|
||
line-height: 1.4;
|
||
height: 72rpx;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
padding: 0 12rpx;
|
||
}
|
||
|
||
.card-tags-row {
|
||
display: flex;
|
||
flex-direction: row;
|
||
flex-wrap: wrap;
|
||
padding: 0 12rpx;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
|
||
.card-tag {
|
||
height: 28rpx;
|
||
line-height: 28rpx;
|
||
padding: 0 8rpx;
|
||
border-radius: 8rpx;
|
||
font-size: 18rpx;
|
||
font-weight: 700;
|
||
color: #fff7d1;
|
||
background: #e1251b;
|
||
margin-right: 6rpx;
|
||
margin-bottom: 4rpx;
|
||
}
|
||
|
||
.card-highlight {
|
||
font-size: 20rpx;
|
||
line-height: 28rpx;
|
||
color: #7a7a7a;
|
||
padding: 0 12rpx;
|
||
margin-bottom: 8rpx;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.service-tags-row {
|
||
display: flex;
|
||
flex-direction: row;
|
||
flex-wrap: wrap;
|
||
padding: 0 12rpx;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
|
||
.service-tag {
|
||
height: 28rpx;
|
||
line-height: 28rpx;
|
||
padding: 0 8rpx;
|
||
border-radius: 8rpx;
|
||
font-size: 18rpx;
|
||
font-weight: 600;
|
||
color: #12b76a;
|
||
background: #ecfdf3;
|
||
margin-right: 6rpx;
|
||
margin-bottom: 4rpx;
|
||
}
|
||
|
||
.guess-bottom {
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 0 12rpx 12rpx;
|
||
}
|
||
|
||
.price-stack {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.guess-price {
|
||
font-size: 32rpx;
|
||
color: #ff4d1a;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.market-price {
|
||
font-size: 20rpx;
|
||
line-height: 1.2;
|
||
color: #9a9a9a;
|
||
text-decoration: line-through;
|
||
margin-top: 4rpx;
|
||
}
|
||
|
||
.guess-add-btn {
|
||
width: 44rpx;
|
||
height: 44rpx;
|
||
background-color: #ff4d1a;
|
||
border-radius: 22rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.guess-add-icon {
|
||
color: #fff;
|
||
font-size: 28rpx;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.search-suggestions {
|
||
background-color: #ffffff;
|
||
border-radius: 20rpx;
|
||
padding: 0 24rpx;
|
||
margin-bottom: 24rpx;
|
||
}
|
||
|
||
.suggestion-item {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
height: 88rpx;
|
||
border-bottom: 1rpx solid #f5f5f7;
|
||
}
|
||
|
||
.suggestion-icon {
|
||
margin-right: 16rpx;
|
||
font-size: 28rpx;
|
||
color: #999;
|
||
}
|
||
|
||
.suggestion-text {
|
||
font-size: 28rpx;
|
||
color: #222222;
|
||
}
|
||
|
||
.results-header {
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 16rpx 24rpx;
|
||
background-color: #ffffff;
|
||
margin-bottom: 2rpx;
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
border-radius: 20rpx 20rpx 0 0;
|
||
}
|
||
|
||
.results-title {
|
||
font-size: 30rpx;
|
||
font-weight: bold;
|
||
color: #222222;
|
||
}
|
||
|
||
.filter-tabs {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
justify-content: flex-start;
|
||
}
|
||
|
||
.filter-tab {
|
||
font-size: 26rpx;
|
||
color: #666;
|
||
padding: 12rpx 24rpx;
|
||
border-radius: 999rpx;
|
||
border: 1rpx solid #e0e0e0;
|
||
white-space: nowrap;
|
||
text-align: center;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
margin-left: 12rpx;
|
||
}
|
||
|
||
.filter-tab.active {
|
||
background: #ff4d1a;
|
||
color: white;
|
||
border-color: #ff4d1a;
|
||
}
|
||
|
||
.search-results {
|
||
padding-bottom: 20rpx;
|
||
width: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.results-list {
|
||
display: flex;
|
||
flex-direction: row;
|
||
flex-wrap: wrap;
|
||
justify-content: flex-start;
|
||
padding: 16rpx;
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
margin-top: 8rpx;
|
||
background-color: #ffffff;
|
||
border-radius: 0 0 20rpx 20rpx;
|
||
}
|
||
|
||
.result-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
background: #ffffff;
|
||
border-radius: 20rpx;
|
||
overflow: hidden;
|
||
width: 48%;
|
||
margin-bottom: 20rpx;
|
||
margin-right: 2%;
|
||
border: 1rpx 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%;
|
||
margin-right: 2%;
|
||
}
|
||
}
|
||
|
||
@media screen and (min-width: 1400px) {
|
||
.result-item {
|
||
width: 23%;
|
||
}
|
||
}
|
||
|
||
.product-image {
|
||
width: 100%;
|
||
height: 280rpx;
|
||
object-fit: cover;
|
||
background-color: #f5f5f7;
|
||
border-radius: 16rpx;
|
||
margin-bottom: 12rpx;
|
||
}
|
||
|
||
.product-name {
|
||
font-size: 26rpx;
|
||
color: #222222;
|
||
margin-bottom: 8rpx;
|
||
line-height: 1.4;
|
||
height: 72rpx;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
padding: 0 12rpx;
|
||
}
|
||
|
||
.product-bottom {
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 0 12rpx 12rpx;
|
||
}
|
||
|
||
.product-price {
|
||
font-size: 32rpx;
|
||
color: #ff4d1a;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.card-sales-text {
|
||
font-size: 20rpx;
|
||
line-height: 28rpx;
|
||
color: #8f8f8f;
|
||
padding: 0 12rpx 12rpx;
|
||
}
|
||
|
||
.product-add-btn {
|
||
width: 44rpx;
|
||
height: 44rpx;
|
||
background-color: #ff4d1a;
|
||
border-radius: 22rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.add-icon {
|
||
color: #fff;
|
||
font-size: 28rpx;
|
||
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>
|
||
|
||
|
||
|