24 changed files with 519 additions and 69 deletions
@ -0,0 +1,33 @@ |
|||||
|
// 使用与首页相同的样式,主要依赖Tailwind CSS类名 |
||||
|
.buy-btn { |
||||
|
background: linear-gradient(to right, #1cd98a, #24ca94); |
||||
|
border-radius: 20px; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
position: relative; |
||||
|
overflow: hidden; |
||||
|
|
||||
|
.cart-icon { |
||||
|
position: absolute; |
||||
|
left: 0; |
||||
|
top: 0; |
||||
|
bottom: 0; |
||||
|
width: 40px; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
background: rgba(0, 0, 0, 0.1); |
||||
|
border-radius: 20px 0 0 20px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.car-no { |
||||
|
font-weight: 500; |
||||
|
color: #333; |
||||
|
line-height: 1.4; |
||||
|
display: -webkit-box; |
||||
|
-webkit-box-orient: vertical; |
||||
|
-webkit-line-clamp: 2; |
||||
|
overflow: hidden; |
||||
|
} |
@ -0,0 +1,58 @@ |
|||||
|
import { View } from '@tarojs/components' |
||||
|
import { Image } from '@nutui/nutui-react-taro' |
||||
|
import { Share } from '@nutui/icons-react-taro' |
||||
|
import Taro from '@tarojs/taro' |
||||
|
import { ShopGoods } from '@/api/shop/shopGoods/model' |
||||
|
import './GoodsItem.scss' |
||||
|
|
||||
|
interface GoodsItemProps { |
||||
|
goods: ShopGoods |
||||
|
} |
||||
|
|
||||
|
const GoodsItem = ({ goods }: GoodsItemProps) => { |
||||
|
// 跳转到商品详情
|
||||
|
const goToDetail = () => { |
||||
|
Taro.navigateTo({ |
||||
|
url: `/shop/goodsDetail/index?id=${goods.goodsId}` |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
return ( |
||||
|
<div className={'flex flex-col rounded-lg bg-white shadow-sm w-full mb-5'}> |
||||
|
<Image |
||||
|
src={goods.image || ''} |
||||
|
mode={'aspectFit'} |
||||
|
lazyLoad={false} |
||||
|
radius="10px 10px 0 0" |
||||
|
height="180" |
||||
|
onClick={goToDetail} |
||||
|
/> |
||||
|
<div className={'flex flex-col p-2 rounded-lg'}> |
||||
|
<div> |
||||
|
<div className={'car-no text-sm'}>{goods.name || goods.goodsName}</div> |
||||
|
<div className={'flex justify-between text-xs py-1'}> |
||||
|
<span className={'text-orange-500'}>{goods.comments || ''}</span> |
||||
|
<span className={'text-gray-400'}>已售 {goods.sales || 0}</span> |
||||
|
</div> |
||||
|
<div className={'flex justify-between items-center py-2'}> |
||||
|
<div className={'flex text-red-500 text-xl items-baseline'}> |
||||
|
<span className={'text-xs'}>¥</span> |
||||
|
<span className={'font-bold text-2xl'}>{goods.price || '0.00'}</span> |
||||
|
</div> |
||||
|
<div className={'buy-btn'}> |
||||
|
<div className={'cart-icon'}> |
||||
|
<Share size={20} className={'mx-4 mt-2'} |
||||
|
onClick={goToDetail}/> |
||||
|
</div> |
||||
|
<div className={'text-white pl-4 pr-5'} |
||||
|
onClick={goToDetail}>购买 |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export default GoodsItem |
@ -0,0 +1,3 @@ |
|||||
|
export default definePageConfig({ |
||||
|
navigationBarTitleText: '商品搜索' |
||||
|
}) |
@ -0,0 +1,103 @@ |
|||||
|
.search-page { |
||||
|
min-height: 100vh; |
||||
|
background: #f5f5f5; |
||||
|
|
||||
|
// 搜索输入框样式 |
||||
|
.search-input-wrapper { |
||||
|
flex: 1; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
background: #f5f5f5; |
||||
|
border-radius: 20px; |
||||
|
padding: 0 12px; |
||||
|
|
||||
|
.search-icon { |
||||
|
color: #999; |
||||
|
margin-right: 8px; |
||||
|
} |
||||
|
|
||||
|
.search-input { |
||||
|
flex: 1; |
||||
|
border: none; |
||||
|
background: transparent; |
||||
|
font-size: 14px; |
||||
|
|
||||
|
input { |
||||
|
background: transparent !important; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.search-btn { |
||||
|
padding: 0 16px; |
||||
|
height: 36px; |
||||
|
border-radius: 18px; |
||||
|
font-size: 14px; |
||||
|
} |
||||
|
|
||||
|
.search-content { |
||||
|
padding-top: calc(32px + env(safe-area-inset-top)); |
||||
|
|
||||
|
.search-history { |
||||
|
background: #fff; |
||||
|
margin-bottom: 8px; |
||||
|
|
||||
|
.history-header { |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
padding: 16px; |
||||
|
border-bottom: 1px solid #f5f5f5; |
||||
|
|
||||
|
.history-title { |
||||
|
font-size: 16px; |
||||
|
font-weight: 500; |
||||
|
color: #333; |
||||
|
} |
||||
|
|
||||
|
.clear-btn { |
||||
|
font-size: 14px; |
||||
|
color: #999; |
||||
|
cursor: pointer; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.history-list { |
||||
|
padding: 16px; |
||||
|
display: flex; |
||||
|
flex-wrap: wrap; |
||||
|
gap: 12px; |
||||
|
|
||||
|
.history-item { |
||||
|
padding: 8px 16px; |
||||
|
background: #f5f5f5; |
||||
|
border-radius: 16px; |
||||
|
color: #666; |
||||
|
cursor: pointer; |
||||
|
|
||||
|
&:active { |
||||
|
background: #e5e5e5; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.search-results { |
||||
|
.result-header { |
||||
|
padding: 16px; |
||||
|
color: #666; |
||||
|
background: #fff; |
||||
|
border-bottom: 1px solid #f5f5f5; |
||||
|
margin-bottom: 8px; |
||||
|
} |
||||
|
|
||||
|
.loading-wrapper { |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
padding: 40px 0; |
||||
|
background: #fff; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,237 @@ |
|||||
|
import {useEffect, useState} from 'react' |
||||
|
import {useRouter} from '@tarojs/taro' |
||||
|
import Taro from '@tarojs/taro' |
||||
|
import {View} from '@tarojs/components' |
||||
|
import {Loading, Empty, InfiniteLoading, Input, Button} from '@nutui/nutui-react-taro' |
||||
|
import {Search} from '@nutui/icons-react-taro'; |
||||
|
import {ShopGoods} from '@/api/shop/shopGoods/model' |
||||
|
import {pageShopGoods} from '@/api/shop/shopGoods' |
||||
|
import GoodsItem from './components/GoodsItem' |
||||
|
import './index.scss' |
||||
|
|
||||
|
const SearchPage = () => { |
||||
|
const router = useRouter() |
||||
|
const [keywords, setKeywords] = useState<string>('') |
||||
|
const [goodsList, setGoodsList] = useState<ShopGoods[]>([]) |
||||
|
const [loading, setLoading] = useState(false) |
||||
|
const [page, setPage] = useState(1) |
||||
|
const [hasMore, setHasMore] = useState(true) |
||||
|
const [total, setTotal] = useState(0) |
||||
|
const [searchHistory, setSearchHistory] = useState<string[]>([]) |
||||
|
|
||||
|
// 从路由参数获取搜索关键词
|
||||
|
useEffect(() => { |
||||
|
const {keywords: routeKeywords} = router.params || {} |
||||
|
if (routeKeywords) { |
||||
|
setKeywords(decodeURIComponent(routeKeywords)) |
||||
|
handleSearch(decodeURIComponent(routeKeywords), 1).then() |
||||
|
} |
||||
|
|
||||
|
// 加载搜索历史
|
||||
|
loadSearchHistory() |
||||
|
}, []) |
||||
|
|
||||
|
// 加载搜索历史
|
||||
|
const loadSearchHistory = () => { |
||||
|
try { |
||||
|
const history = Taro.getStorageSync('search_history') || [] |
||||
|
setSearchHistory(history) |
||||
|
} catch (error) { |
||||
|
console.error('加载搜索历史失败:', error) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 保存搜索历史
|
||||
|
const saveSearchHistory = (keyword: string) => { |
||||
|
try { |
||||
|
let history = Taro.getStorageSync('search_history') || [] |
||||
|
// 去重并添加到开头
|
||||
|
history = history.filter(item => item !== keyword) |
||||
|
history.unshift(keyword) |
||||
|
// 只保留最近10条
|
||||
|
history = history.slice(0, 10) |
||||
|
Taro.setStorageSync('search_history', history) |
||||
|
setSearchHistory(history) |
||||
|
} catch (error) { |
||||
|
console.error('保存搜索历史失败:', error) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const handleKeywords = (keywords) => { |
||||
|
setKeywords(keywords) |
||||
|
handleSearch(keywords).then() |
||||
|
} |
||||
|
|
||||
|
// 搜索商品
|
||||
|
const handleSearch = async (searchKeywords: string, pageNum: number = 1) => { |
||||
|
if (!searchKeywords.trim()) { |
||||
|
Taro.showToast({ |
||||
|
title: '请输入搜索关键词', |
||||
|
icon: 'none' |
||||
|
}) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
setLoading(true) |
||||
|
|
||||
|
try { |
||||
|
const params = { |
||||
|
keywords: searchKeywords.trim(), |
||||
|
page: pageNum, |
||||
|
size: 10, |
||||
|
isShow: 1 // 只搜索上架商品
|
||||
|
} |
||||
|
|
||||
|
const result = await pageShopGoods(params) |
||||
|
|
||||
|
if (pageNum === 1) { |
||||
|
setGoodsList(result?.list || []) |
||||
|
setTotal(result?.count || 0) |
||||
|
// 保存搜索历史
|
||||
|
saveSearchHistory(searchKeywords.trim()) |
||||
|
} else { |
||||
|
setGoodsList(prev => [...prev, ...(result?.list || [])]) |
||||
|
} |
||||
|
|
||||
|
setHasMore((result?.list?.length || 0) >= 10) |
||||
|
setPage(pageNum) |
||||
|
|
||||
|
} catch (error) { |
||||
|
console.error('搜索失败:', error) |
||||
|
Taro.showToast({ |
||||
|
title: '搜索失败,请重试', |
||||
|
icon: 'none' |
||||
|
}) |
||||
|
} finally { |
||||
|
setLoading(false) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 加载更多
|
||||
|
const loadMore = () => { |
||||
|
if (!loading && hasMore && keywords.trim()) { |
||||
|
handleSearch(keywords, page + 1).then() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 点击历史搜索
|
||||
|
const onHistoryClick = (keyword: string) => { |
||||
|
setKeywords(keyword) |
||||
|
setPage(1) |
||||
|
handleSearch(keyword, 1) |
||||
|
} |
||||
|
|
||||
|
// 清空搜索历史
|
||||
|
const clearHistory = () => { |
||||
|
Taro.showModal({ |
||||
|
title: '提示', |
||||
|
content: '确定要清空搜索历史吗?', |
||||
|
success: (res) => { |
||||
|
if (res.confirm) { |
||||
|
try { |
||||
|
Taro.removeStorageSync('search_history') |
||||
|
setSearchHistory([]) |
||||
|
} catch (error) { |
||||
|
console.error('清空搜索历史失败:', error) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
return ( |
||||
|
<View className="search-page pt-3"> |
||||
|
<div className={'px-2'}> |
||||
|
<div |
||||
|
style={{ |
||||
|
display: 'flex', |
||||
|
alignItems: 'center', |
||||
|
background: '#ffffff', |
||||
|
padding: '0 5px', |
||||
|
borderRadius: '20px', |
||||
|
marginTop: '5px', |
||||
|
}} |
||||
|
> |
||||
|
<Search size={18} className={'ml-2 text-gray-400'}/> |
||||
|
<Input |
||||
|
placeholder="搜索商品" |
||||
|
value={keywords} |
||||
|
onChange={handleKeywords} |
||||
|
onConfirm={() => handleSearch(keywords)} |
||||
|
style={{padding: '9px 8px'}} |
||||
|
/> |
||||
|
<div |
||||
|
className={'flex items-center'} |
||||
|
> |
||||
|
<Button type="success" style={{background: 'linear-gradient(to bottom, #1cd98a, #24ca94)'}} |
||||
|
onClick={() => handleSearch(keywords)}> |
||||
|
搜索 |
||||
|
</Button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
{/*<SearchBar style={{height: `${statusBarHeight}px`}} shape="round" placeholder="搜索商品" onChange={setKeywords} onSearch={handleSearch}/>*/} |
||||
|
|
||||
|
{/* 搜索内容 */} |
||||
|
<View className="search-content"> |
||||
|
{/* 搜索历史 */} |
||||
|
{!keywords && searchHistory.length > 0 && ( |
||||
|
<View className="search-history"> |
||||
|
<View className="history-header"> |
||||
|
<View className="text-sm">搜索历史</View> |
||||
|
<View className={'text-gray-400'} onClick={clearHistory}>清空</View> |
||||
|
</View> |
||||
|
<View className="history-list"> |
||||
|
{searchHistory.map((item, index) => ( |
||||
|
<View |
||||
|
key={index} |
||||
|
className="history-item" |
||||
|
onClick={() => onHistoryClick(item)} |
||||
|
> |
||||
|
{item} |
||||
|
</View> |
||||
|
))} |
||||
|
</View> |
||||
|
</View> |
||||
|
)} |
||||
|
|
||||
|
{/* 搜索结果 */} |
||||
|
{keywords && ( |
||||
|
<View className="search-results"> |
||||
|
{/* 结果统计 */} |
||||
|
<View className="result-header"> |
||||
|
找到 {total} 件相关商品 |
||||
|
</View> |
||||
|
|
||||
|
{/* 商品列表 */} |
||||
|
{loading && page === 1 ? ( |
||||
|
<View className="loading-wrapper"> |
||||
|
<Loading>搜索中...</Loading> |
||||
|
</View> |
||||
|
) : goodsList.length > 0 ? ( |
||||
|
<div className={'py-3'}> |
||||
|
<div className={'flex flex-col justify-between items-center rounded-lg px-2'}> |
||||
|
<InfiniteLoading |
||||
|
hasMore={hasMore} |
||||
|
// @ts-ignore
|
||||
|
onLoadMore={loadMore} |
||||
|
loadingText="加载中..." |
||||
|
loadMoreText="没有更多了" |
||||
|
> |
||||
|
{goodsList.map((item) => ( |
||||
|
<GoodsItem key={item.goodsId} goods={item}/> |
||||
|
))} |
||||
|
</InfiniteLoading> |
||||
|
</div> |
||||
|
</div> |
||||
|
) : ( |
||||
|
<Empty description="暂无相关商品"/> |
||||
|
)} |
||||
|
</View> |
||||
|
)} |
||||
|
</View> |
||||
|
</View> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export default SearchPage |
@ -0,0 +1,11 @@ |
|||||
|
#!/bin/bash |
||||
|
|
||||
|
# 设置代理(根据你的 Clash Verge 配置) |
||||
|
export http_proxy=http://127.0.0.1:7897 |
||||
|
export https_proxy=http://127.0.0.1:7897 |
||||
|
|
||||
|
# 启动 Claude Code |
||||
|
echo "🚀 启动 Claude Code..." |
||||
|
echo "📡 使用代理: $http_proxy" |
||||
|
|
||||
|
npx @anthropic-ai/claude-code |
Loading…
Reference in new issue