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