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

1238 lines
26 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
scroll-y
class="main-content"
:style="{ height: scrollHeight + 'px' }"
@scrolltolower="loadMore"
>
<!-- 初始状态(无搜索词) -->
<view v-if="!searchKeyword && !showResults">
<!-- 搜索历史 -->
<view v-if="searchHistory.length > 0" class="search-history">
<view class="section-header">
<text class="section-title">搜索历史</text>
<view class="header-right" @click="clearHistory">
<text class="clear-text">清空</text>
<text class="clear-icon-trash">🗑️</text>
</view>
</view>
<view class="history-tags">
<view
v-for="(item, index) in searchHistory"
:key="index"
class="history-tag"
@click="searchFromHistory(item)"
>
<text class="history-text">{{ item }}</text>
<view class="delete-tag-btn" @click.stop="deleteHistoryItem(index)">
<text class="delete-icon">×</text>
</view>
</view>
</view>
</view>
<!-- 热门搜索 -->
<view class="hot-search">
<view class="section-header">
<text class="section-title">热门搜索</text>
</view>
<view class="hot-tags">
<view
v-for="(item, index) in hotSearchList"
:key="index"
class="hot-tag"
:class="{ 'hot': item.hot }"
@click="searchFromHot(item.keyword)"
>
<text class="hot-rank" :class="{ 'top-three': index < 3 }">{{ index + 1 }}</text>
<text class="hot-text">{{ item.keyword }}</text>
<text v-if="item.hot" class="hot-icon">🔥</text>
</view>
</view>
</view>
<!-- 猜你需要 (新增功能) -->
<view class="guess-you-like">
<view class="section-header">
<view class="title-with-icon">
<text class="section-icon">✨</text>
<text class="section-title">猜你需要</text>
</view>
<text class="refresh-btn" @click="refreshGuessList">换一批</text>
</view>
<view class="guess-grid">
<view
v-for="item in guessList"
:key="item.id"
class="guess-item"
@click="viewProductDetail(item)"
>
<view class="guess-img-box">
<image class="guess-img" :src="item.image" mode="aspectFill" />
</view>
<view class="guess-info">
<text class="guess-name">{{ item.name }}</text>
<view class="guess-price-row">
<text class="price-symbol">¥</text>
<text class="price-num">{{ item.price }}</text>
<text class="sales-text">已售{{ item.sales }}</text>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 搜索建议 -->
<view v-if="searchKeyword && !showResults" class="search-suggestions">
<view class="suggestions-list">
<view
v-for="(suggestion, index) in searchSuggestions"
:key="index"
class="suggestion-item"
@click="selectSuggestion(suggestion)"
>
<view class="suggestion-icon">🔍</view>
<text class="suggestion-text">{{ suggestion }}</text>
</view>
</view>
</view>
<!-- 搜索结果 -->
<view v-if="showResults" class="search-results">
<view class="results-header">
<text class="results-title">搜索结果</text>
<view class="filter-tabs">
<text
class="filter-tab"
: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) // 价格排序是否为升序
// 数据定义
const searchHistory = ref<string[]>([])
const hotSearchList = ref<any[]>([])
const guessList = ref<any[]>([])
const searchResults = ref<any[]>([])
// 模拟数据库
const mockDatabase = {
hot: [
{ keyword: '感冒灵', hot: true },
{ keyword: '布洛芬', hot: true },
{ keyword: '口罩', hot: true },
{ keyword: '维生素C', hot: false },
{ keyword: '板蓝根', hot: false },
{ keyword: '创可贴', hot: false },
],
guess: [
{ id: 'g1', name: '医用外科口罩', price: 19.9, image: 'https://picsum.photos/200/200?random=1', sales: '1万+' },
{ id: 'g2', name: '酒精消毒液', price: 9.9, image: 'https://picsum.photos/200/200?random=2', sales: '5000+' },
{ id: 'g3', name: '电子体温计', price: 29.9, image: 'https://picsum.photos/200/200?random=3', sales: '2000+' },
{ id: 'g4', name: '碘伏消毒液', price: 5.5, image: 'https://picsum.photos/200/200?random=4', sales: '1000+' },
{ id: 'g5', name: '退热贴', price: 15.8, image: 'https://picsum.photos/200/200?random=5', sales: '3000+' },
{ id: 'g6', name: '棉签', price: 3.9, image: 'https://picsum.photos/200/200?random=6', sales: '8000+' },
]
}
// 搜索建议
const searchSuggestions = computed(() => {
if (!searchKeyword.value) return []
// 简单模拟
return [
`${searchKeyword.value}胶囊`,
`${searchKeyword.value}颗粒`,
`${searchKeyword.value}片`,
`儿童${searchKeyword.value}`
]
})
onMounted(() => {
initPage()
})
const initPage = () => {
try {
const systemInfo = uni.getSystemInfoSync()
statusBarHeight.value = systemInfo.statusBarHeight || 0
const windowHeight = systemInfo.windowHeight
// 减去头部高度 (约60px + statusBarHeight)
scrollHeight.value = windowHeight - (60 + statusBarHeight.value)
loadData()
// 检查页面参数
const pages = getCurrentPages()
if (pages.length > 0) {
const currentPage = pages[pages.length - 1]
// @ts-ignore
const options = currentPage.options
if (options && options['keyword']) {
const keyword = decodeURIComponent(options['keyword'])
searchKeyword.value = keyword
if (options['type'] === 'family') {
// 如果是家庭常备药类型,直接添加到历史并搜索
addToHistory(keyword)
// 立即显示结果区域并设置为加载中
showResults.value = true
loading.value = true
// 确保searchResults不为空数组导致闪烁虽然loading=true已经拦截了empty-result但双重保险
// 此时不要置空searchResults或者给一个初始值
// 直接调用搜索移除setTimeout防止中间状态
performSearch()
}
}
}
} catch (e) {
console.error('初始化失败', e)
isError.value = true
}
}
// 加载基础数据
const loadData = () => {
// loading.value = true // 不使用全局loading避免影响搜索状态
isError.value = false
// 模拟网络请求
setTimeout(() => {
try {
loadSearchHistory()
hotSearchList.value = mockDatabase.hot
guessList.value = mockDatabase.guess
// loading.value = false // 不使用全局loading
} catch (e) {
isError.value = true
// loading.value = false
}
}, 500)
}
// 点击重试
const retryLoad = () => {
uni.showLoading({ title: '重新加载中' })
setTimeout(() => {
uni.hideLoading()
loadData()
}, 1000)
}
// 历史记录管理
const loadSearchHistory = () => {
const history = uni.getStorageSync('searchHistory')
if (history) {
try {
// 确保是数组
const parsed = JSON.parse(history as string)
if (Array.isArray(parsed)) {
searchHistory.value = parsed as string[]
}
} catch (e) {
searchHistory.value = []
}
}
}
const saveSearchHistory = () => {
uni.setStorageSync('searchHistory', JSON.stringify(searchHistory.value))
}
const addToHistory = (keyword: string) => {
if (!keyword) return
const index = searchHistory.value.indexOf(keyword)
if (index > -1) {
searchHistory.value.splice(index, 1)
}
searchHistory.value.unshift(keyword)
if (searchHistory.value.length > 10) searchHistory.value.pop()
saveSearchHistory()
}
const clearHistory = () => {
uni.showModal({
title: '提示',
content: '确定清空搜索历史吗?',
success: (res) => {
if (res.confirm) {
searchHistory.value = []
uni.removeStorageSync('searchHistory')
}
}
})
}
const deleteHistoryItem = (index: number) => {
searchHistory.value.splice(index, 1)
saveSearchHistory()
}
// 搜索逻辑
const onInput = (e: any) => {
searchKeyword.value = e.detail.value
if (!searchKeyword.value) {
showResults.value = false
}
}
const clearSearch = () => {
searchKeyword.value = ''
showResults.value = false
}
const onSearch = () => {
if (!searchKeyword.value.trim()) return
addToHistory(searchKeyword.value.trim())
performSearch()
}
const searchFromHistory = (keyword: string) => {
searchKeyword.value = keyword
performSearch()
}
const searchFromHot = (keyword: string) => {
searchKeyword.value = keyword
addToHistory(keyword)
performSearch()
}
const selectSuggestion = (suggestion: string) => {
searchKeyword.value = suggestion
addToHistory(suggestion)
performSearch()
}
const currentPage = ref(1)
const performSearch = () => {
// 再次强制设置状态,确保万无一失
showResults.value = true
loading.value = true
// 重置页码
currentPage.value = 1
// 保持旧数据直到新数据回来,或者依靠 loading 状态完全遮罩
// 使用 Supabase 搜索真实数据
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
}
supabaseService.searchProducts(keyword, currentPage.value, 20, sortBy, ascending)
.then((response) => {
searchResults.value = response.data as any[]
hasMore.value = response.hasmore
loading.value = false
// 如果无结果,显示空状态
if (searchResults.value.length === 0) {
// empty-result 组件会自动显示
}
})
.catch((error) => {
console.error('搜索失败:', error)
loading.value = false
// 可以显示错误提示,但为了用户体验,先不显示
// 保持搜索结果为空让empty-result显示
})
}
// 切换排序
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 = () => {
if (loading.value || !hasMore.value || !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
}
supabaseService.searchProducts(keyword, currentPage.value, 20, sortBy, ascending)
.then((response) => {
searchResults.value.push(...(response.data as any[]))
hasMore.value = response.hasmore
loading.value = false
})
.catch((error) => {
console.error('加载更多失败:', error)
loading.value = false
// 加载失败时,假设没有更多数据
hasMore.value = false
})
}
const refreshGuessList = () => {
uni.showLoading({ title: '刷新中' })
setTimeout(() => {
guessList.value = guessList.value.sort(() => Math.random() - 0.5)
uni.hideLoading()
}, 500)
}
const viewProductDetail = (item: any) => {
// 跳转详情页逻辑
console.log('查看商品', item)
uni.navigateTo({
url: `/pages/mall/consumer/product-detail?productId=${item.id}&price=${item.price}&originalPrice=${item.original_price || ''}&name=${encodeURIComponent(item.name)}&image=${encodeURIComponent(item.image)}`
})
}
// 添加到购物车
const addToCart = (product: any) => {
// 获取现有购物车数据
const cartData = uni.getStorageSync('cart')
let cartItems: any[] = []
if (cartData) {
try {
cartItems = JSON.parse(cartData as string) as any[]
} catch (e) {
console.error('解析购物车数据失败', e)
}
}
// 检查商品是否已存在
const existingItem = cartItems.find((item: any) => item.id === product.id)
if (existingItem) {
existingItem.quantity++
} else {
// 添加新商品
cartItems.push({
id: product.id,
shopId: product.shopId || 'shop_search_default',
shopName: product.shopName || (product.tag === '自营' ? '平台自营大药房' : '优质大药房'),
name: product.name,
price: product.price,
image: product.image,
spec: product.specification || '默认规格',
quantity: 1,
selected: true
})
}
// 保存回存储
uni.setStorageSync('cart', JSON.stringify(cartItems))
uni.showToast({
title: '已添加到购物车',
icon: 'success'
})
}
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 {
// 如果在搜索初始页,则返回上一页
uni.navigateBack()
}
}
</script>
<style>
.search-page {
width: 100%;
height: 100vh;
background-color: #f5f5f5;
display: flex;
flex-direction: column;
}
/* 头部样式 */
.search-header {
background-color: #ffffff;
padding-bottom: 10px;
/* #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;
gap: 12px;
width: 100%; /* 确保占满宽度 */
}
.back-btn {
padding: 4px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
width: 32px; /* 固定宽度防止压缩 */
height: 32px;
}
.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: 500;
}
/* 内容区域 */
.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;
flex-shrink: 0; /* 防止被压缩 */
}
.clear-text {
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;
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;
flex-shrink: 0; /* 防止被压缩 */
}
.history-text {
font-size: 13px;
color: #666;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.delete-tag-btn {
display: flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
border-radius: 50%;
background-color: #f0f0f0;
}
.delete-icon {
font-size: 12px;
color: #999;
line-height: 1;
}
/* 热门搜索 */
.hot-search {
margin-bottom: 24px;
}
.hot-tags {
display: flex;
flex-direction: row; /* UVUE 显式设置 row */
flex-wrap: wrap; /* 允许换行 */
gap: 10px;
padding: 0 4px;
}
.hot-tag {
background-color: #fff;
padding: 6px 12px;
border-radius: 16px; /* 增加圆角,像胶囊一样 */
display: flex;
flex-direction: row;
align-items: center;
gap: 4px;
flex-shrink: 0; /* 防止被压缩 */
}
.hot-tag.hot {
background-color: #fff0f0;
}
.hot-rank {
font-size: 12px;
color: #999;
font-weight: bold;
margin-right: 2px;
}
.hot-rank.top-three {
color: #ff5000;
}
.hot-text {
font-size: 13px;
color: #333;
}
.hot-icon {
font-size: 12px;
}
/* 猜你需要 */
.guess-you-like {
margin-bottom: 20px;
}
.title-with-icon {
display: flex;
align-items: center;
gap: 6px;
}
.section-icon {
font-size: 16px;
}
.refresh-btn {
font-size: 12px;
color: #4CAF50;
}
.guess-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
.guess-item {
background-color: #fff;
border-radius: 8px;
overflow: hidden;
padding-bottom: 8px;
}
.guess-img-box {
width: 100%;
height: 0;
padding-bottom: 100%;
position: relative;
background-color: #f0f0f0;
}
.guess-img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.guess-info {
padding: 8px;
}
.guess-name {
font-size: 13px;
color: #333;
margin-bottom: 6px;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
.guess-price-row {
display: flex;
align-items: baseline;
}
.price-symbol {
font-size: 12px;
color: #ff5000;
}
.price-num {
font-size: 16px;
color: #ff5000;
font-weight: bold;
margin-right: 6px;
}
.sales-text {
font-size: 10px;
color: #999;
}
/* 搜索建议列表 */
.search-suggestions {
background-color: #fff;
border-radius: 8px;
padding: 0 12px;
}
.suggestion-item {
display: flex;
align-items: center;
height: 44px;
border-bottom: 1px solid #f5f5f5;
}
.suggestion-icon {
margin-right: 10px;
font-size: 14px;
color: #999;
}
.suggestion-text {
font-size: 14px;
color: #333;
}
/* 搜索结果 */
.search-results {
padding-bottom: 20px;
}
.results-header {
display: flex;
flex-direction: row; /* UVUE 显式设置 row */
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
flex-wrap: wrap; /* 允许换行以适应小屏 */
gap: 8px;
}
.results-title {
font-size: 15px;
font-weight: bold;
color: #333;
}
.filter-tabs {
display: flex;
flex-direction: row; /* UVUE 显式设置 row */
gap: 16px;
flex: 1; /* 自适应填充剩余空间 */
justify-content: flex-end; /* 靠右对齐 */
}
.filter-tab {
font-size: 13px;
color: #666;
padding: 4px 8px; /* 增加点击区域 */
}
.filter-tab.active {
color: #4CAF50;
font-weight: 500;
}
.results-list {
display: grid;
grid-template-columns: repeat(2, 1fr); /* 默认移动端双列 */
gap: 10px;
padding: 0 4px;
}
/* 响应式布局 */
/* 平板设备 (768px以上) */
@media screen and (min-width: 768px) {
.results-list {
grid-template-columns: repeat(3, 1fr); /* 平板显示3列 */
gap: 16px;
padding: 0 16px;
}
.guess-grid {
grid-template-columns: repeat(4, 1fr); /* 猜你喜欢在平板上显示4列 */
}
}
/* 桌面设备 (1024px以上) */
@media screen and (min-width: 1024px) {
.results-list {
grid-template-columns: repeat(4, 1fr); /* 桌面显示4列 */
gap: 20px;
padding: 0 24px;
}
.guess-grid {
grid-template-columns: repeat(6, 1fr); /* 猜你喜欢在桌面上显示6列 */
}
/* 桌面端调整图片高度 */
.product-image {
height: 160px;
}
}
.result-item {
background-color: #fff;
border-radius: 8px;
padding: 8px;
display: flex;
flex-direction: column; /* 垂直排列 */
gap: 8px;
}
.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;
}
.product-name {
font-size: 13px; /* 减小字号 */
color: #333;
font-weight: 500;
line-height: 1.3;
height: 34px; /* 限制高度 */
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.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: baseline;
}
.price-symbol {
font-size: 10px;
}
.price-value {
font-size: 16px; /* 减小价格字号 */
font-weight: 600;
}
.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;
gap: 12px;
}
.error-icon {
font-size: 48px;
}
.error-title {
font-size: 16px;
font-weight: bold;
color: #333;
}
.error-desc {
font-size: 14px;
color: #999;
}
/* 加载更多 */
.loading-more {
padding: 20px 0;
display: flex;
flex-direction: column;
align-items: center;
}
.loading-spinner {
width: 24px;
height: 24px;
border: 2px solid #f0f0f0;
border-top-color: #4CAF50;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 8px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-text {
font-size: 12px;
color: #999;
}
.no-more {
padding: 20px 0;
text-align: center;
}
.no-more-text {
font-size: 12px;
color: #ccc;
}
.empty-result {
display: flex;
flex-direction: column;
align-items: center;
padding: 40px 0;
}
.empty-icon {
font-size: 40px;
margin-bottom: 12px;
}
.empty-text {
font-size: 14px;
color: #666;
margin-bottom: 4px;
}
.empty-sub {
font-size: 12px;
color: #999;
}
.safe-area {
height: 20px;
}
</style>