init
This commit is contained in:
+306
-28
@@ -1,34 +1,312 @@
|
||||
import { View } from '@tarojs/components'
|
||||
import { Button } from '@antmjs/vantui'
|
||||
import { useCallback, useMemo, useRef, useState } from 'react'
|
||||
import Taro, { useDidShow, useReachBottom } from '@tarojs/taro'
|
||||
import { View, Text, Image, ScrollView } from '@tarojs/components'
|
||||
import { Button, Empty, Popup, Search, Stepper } from '@antmjs/vantui'
|
||||
import useCartStore from '@/stores/cart/useCartStore'
|
||||
import { getCategoriesApi, getProductListApi } from '@/services/product'
|
||||
import { getProductCover } from '@/types/product'
|
||||
import type { Category, Product } from '@/types/product'
|
||||
import './index.less'
|
||||
|
||||
export default function Index() {
|
||||
const PAGE_SIZE = 10
|
||||
/** 存储 key:首页点击分类跳转时经本地存储传参(switchTab 无法带参) */
|
||||
const PENDING_CATEGORY_KEY = 'product_category_id'
|
||||
|
||||
export default function ProductPage() {
|
||||
const addItem = useCartStore(s => s.addItem)
|
||||
|
||||
/** 分类树 */
|
||||
const [categories, setCategories] = useState<Category[]>([])
|
||||
/** 选中的顶级分类(null = 全部) */
|
||||
const [activeTop, setActiveTop] = useState<number | null>(null)
|
||||
/** 选中的子分类(null = 顶级分类下全部) */
|
||||
const [activeChild, setActiveChild] = useState<number | null>(null)
|
||||
/** 已提交的搜索词(onSearch 才生效) */
|
||||
const [searchKey, setSearchKey] = useState('')
|
||||
/** 输入框内容 */
|
||||
const [inputKey, setInputKey] = useState('')
|
||||
|
||||
/** 商品列表 */
|
||||
const [products, setProducts] = useState<Product[]>([])
|
||||
const [page, setPage] = useState(1)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [finished, setFinished] = useState(false)
|
||||
const listLoadingRef = useRef(false)
|
||||
|
||||
/** 加购弹层 */
|
||||
const [showPopup, setShowPopup] = useState(false)
|
||||
const [current, setCurrent] = useState<Product | null>(null)
|
||||
const [qty, setQty] = useState(1)
|
||||
const addingRef = useRef(false)
|
||||
|
||||
/** 选中的顶级分类的子分类 */
|
||||
const childCategories = useMemo(() => {
|
||||
const top = categories.find(c => c.id === activeTop)
|
||||
return top?.children ?? []
|
||||
}, [categories, activeTop])
|
||||
|
||||
/** 当前生效的分类ID(子分类优先) */
|
||||
const effectiveCategoryId = useMemo(
|
||||
() => activeChild ?? activeTop ?? undefined,
|
||||
[activeChild, activeTop],
|
||||
)
|
||||
|
||||
/** 拉取商品列表 */
|
||||
const fetchList = useCallback(
|
||||
async (pageNum: number, reset: boolean) => {
|
||||
if (listLoadingRef.current) return
|
||||
listLoadingRef.current = true
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await getProductListApi({
|
||||
category_id: effectiveCategoryId,
|
||||
keyword: searchKey || undefined,
|
||||
page: pageNum,
|
||||
pageSize: PAGE_SIZE,
|
||||
})
|
||||
const { data, total: totalCount } = res.data
|
||||
setProducts(prev => (reset ? data : [...prev, ...data]))
|
||||
setPage(pageNum)
|
||||
setFinished(pageNum * PAGE_SIZE >= totalCount)
|
||||
} catch {
|
||||
// 错误已由 request 层 toast
|
||||
} finally {
|
||||
listLoadingRef.current = false
|
||||
setLoading(false)
|
||||
}
|
||||
},
|
||||
[effectiveCategoryId, searchKey],
|
||||
)
|
||||
|
||||
/** 分类树(加载完成后处理首页跳转带入的分类) */
|
||||
const loadCategories = useCallback(async () => {
|
||||
try {
|
||||
const res = await getCategoriesApi()
|
||||
setCategories(res.data)
|
||||
// 处理首页跳转带入的分类(switchTab 无法带参,经本地存储传递)
|
||||
let pending: number | null = null
|
||||
try {
|
||||
pending = Taro.getStorageSync(PENDING_CATEGORY_KEY) || null
|
||||
Taro.removeStorageSync(PENDING_CATEGORY_KEY)
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
if (pending) {
|
||||
const top = res.data.find(c => c.id === pending)
|
||||
const topOfChild = res.data.find(c => c.children.some(ch => ch.id === pending))
|
||||
if (top) {
|
||||
setActiveTop(pending)
|
||||
setActiveChild(null)
|
||||
} else if (topOfChild) {
|
||||
setActiveTop(topOfChild.id)
|
||||
setActiveChild(pending)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// 错误已由 request 层 toast
|
||||
}
|
||||
}, [])
|
||||
|
||||
useDidShow(() => {
|
||||
loadCategories()
|
||||
fetchList(1, true)
|
||||
})
|
||||
|
||||
useReachBottom(() => {
|
||||
if (!finished && !listLoadingRef.current) {
|
||||
fetchList(page + 1, false)
|
||||
}
|
||||
})
|
||||
|
||||
/** 切换顶级分类 */
|
||||
const handleTopTap = useCallback((id: number | null) => {
|
||||
setActiveTop(id)
|
||||
setActiveChild(null)
|
||||
}, [])
|
||||
|
||||
/** 切换子分类 */
|
||||
const handleChildTap = useCallback((id: number | null) => {
|
||||
setActiveChild(id)
|
||||
}, [])
|
||||
|
||||
/** 提交搜索 */
|
||||
const handleSearch = useCallback(() => {
|
||||
setSearchKey(inputKey.trim())
|
||||
}, [inputKey])
|
||||
|
||||
/** 清空搜索 */
|
||||
const handleClear = useCallback(() => {
|
||||
setInputKey('')
|
||||
setSearchKey('')
|
||||
}, [])
|
||||
|
||||
/** 打开加购弹层 */
|
||||
const handleAddTap = useCallback((product: Product) => {
|
||||
setCurrent(product)
|
||||
setQty(1)
|
||||
setShowPopup(true)
|
||||
}, [])
|
||||
|
||||
/** 确认加购 */
|
||||
const handleConfirmAdd = useCallback(async () => {
|
||||
if (!current || addingRef.current) return
|
||||
addingRef.current = true
|
||||
try {
|
||||
await addItem(current.id, qty)
|
||||
Taro.showToast({ title: '已加入购物车', icon: 'success' })
|
||||
setShowPopup(false)
|
||||
} catch {
|
||||
// 错误(未设等级价/数量上限)已由 request 层 toast
|
||||
} finally {
|
||||
addingRef.current = false
|
||||
}
|
||||
}, [current, qty, addItem])
|
||||
|
||||
return (
|
||||
<View className='index'>
|
||||
<View><Button type='primary'>Hello world!</Button></View>
|
||||
<View>上面的按钮的颜色已经通过全局主题重写覆盖了,参见src/styles/index.less</View>
|
||||
<View className='product-page'>
|
||||
{/* ========== 搜索 ========== */}
|
||||
<View className='product-search'>
|
||||
<Search
|
||||
value={inputKey}
|
||||
placeholder='搜索品名/规格'
|
||||
shape='round'
|
||||
background='#f7f8fa'
|
||||
onChange={e => setInputKey(String(e.detail))}
|
||||
onSearch={handleSearch}
|
||||
onClear={handleClear}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View className='product-body'>
|
||||
{/* ========== 左侧分类 ========== */}
|
||||
<ScrollView scrollY className='product-categories'>
|
||||
<View
|
||||
className={`category-item ${activeTop === null ? 'active' : ''}`}
|
||||
onClick={() => handleTopTap(null)}
|
||||
>
|
||||
<Text className='category-item__name'>全部</Text>
|
||||
</View>
|
||||
{categories.map(cat => (
|
||||
<View
|
||||
key={cat.id}
|
||||
className={`category-item ${activeTop === cat.id ? 'active' : ''}`}
|
||||
onClick={() => handleTopTap(cat.id)}
|
||||
>
|
||||
<Text className='category-item__name'>{cat.name}</Text>
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
|
||||
{/* ========== 右侧商品列表 ========== */}
|
||||
<View className='product-main'>
|
||||
{/* 子分类 chips */}
|
||||
{childCategories.length > 0 && (
|
||||
<ScrollView scrollX className='child-scroll'>
|
||||
<View
|
||||
className={`child-chip ${activeChild === null ? 'active' : ''}`}
|
||||
onClick={() => handleChildTap(null)}
|
||||
>
|
||||
<Text>全部</Text>
|
||||
</View>
|
||||
{childCategories.map(child => (
|
||||
<View
|
||||
key={child.id}
|
||||
className={`child-chip ${activeChild === child.id ? 'active' : ''}`}
|
||||
onClick={() => handleChildTap(child.id)}
|
||||
>
|
||||
<Text>{child.name}</Text>
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
)}
|
||||
|
||||
{/* 商品列表 */}
|
||||
{products.length === 0 && !loading ? (
|
||||
<Empty description='暂无商品' className='product-empty' />
|
||||
) : (
|
||||
products.map(product => (
|
||||
<View key={product.id} className='product-item'>
|
||||
<Image
|
||||
className='product-item__image'
|
||||
src={getProductCover(product) || 'https://img.yzcdn.cn/vant/cat.jpeg'}
|
||||
mode='aspectFill'
|
||||
lazyLoad
|
||||
/>
|
||||
<View className='product-item__info'>
|
||||
<Text className='product-item__name'>{product.name}</Text>
|
||||
<Text className='product-item__spec'>{product.spec} / {product.unit}</Text>
|
||||
<View className='product-item__bottom'>
|
||||
{product.price !== null ? (
|
||||
<Text className='product-item__price'>¥{product.price}</Text>
|
||||
) : (
|
||||
<Text className='product-item__price product-item__price--none'>价格待定</Text>
|
||||
)}
|
||||
<View className='product-item__add' onClick={() => handleAddTap(product)}>
|
||||
<Text className='product-item__add-icon'>+</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
|
||||
{/* 加载状态 */}
|
||||
{loading && <View className='product-loading'><Text>加载中...</Text></View>}
|
||||
{finished && products.length > 0 && (
|
||||
<View className='product-loading'><Text>没有更多了</Text></View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* ========== 加购弹层 ========== */}
|
||||
<Popup
|
||||
show={showPopup}
|
||||
position='bottom'
|
||||
round
|
||||
closeable
|
||||
closeOnClickOverlay
|
||||
safeAreaInsetBottom
|
||||
onClose={() => setShowPopup(false)}
|
||||
>
|
||||
{current && (
|
||||
<View className='add-popup'>
|
||||
<View className='add-popup__product'>
|
||||
<Image
|
||||
className='add-popup__image'
|
||||
src={getProductCover(current) || 'https://img.yzcdn.cn/vant/cat.jpeg'}
|
||||
mode='aspectFill'
|
||||
/>
|
||||
<View className='add-popup__info'>
|
||||
<Text className='add-popup__name'>{current.name}</Text>
|
||||
<Text className='add-popup__spec'>{current.spec} / {current.unit}</Text>
|
||||
{current.price !== null ? (
|
||||
<Text className='add-popup__price'>¥{current.price}</Text>
|
||||
) : (
|
||||
<Text className='add-popup__price add-popup__price--none'>价格待定</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
<View className='add-popup__row'>
|
||||
<Text className='add-popup__label'>购买数量</Text>
|
||||
<Stepper
|
||||
value={qty}
|
||||
min={1}
|
||||
max={99999999.99}
|
||||
onChange={e => setQty(Number(e.detail))}
|
||||
/>
|
||||
</View>
|
||||
<Button
|
||||
type='danger'
|
||||
block
|
||||
round
|
||||
className='add-popup__submit'
|
||||
onClick={handleConfirmAdd}
|
||||
>
|
||||
加入购物车
|
||||
</Button>
|
||||
</View>
|
||||
)}
|
||||
</Popup>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
// export default class Index extends Component {
|
||||
|
||||
// componentWillMount () { }
|
||||
|
||||
// componentDidMount () { }
|
||||
|
||||
// componentWillUnmount () { }
|
||||
|
||||
// componentDidShow () { }
|
||||
|
||||
// componentDidHide () { }
|
||||
|
||||
// render () {
|
||||
// return (
|
||||
// <View className='index'>
|
||||
// <View><Button type='primary'>Hello world!</Button></View>
|
||||
// <View>上面的按钮的颜色已经通过全局主题重写覆盖了,参见src/style/index.less</View>
|
||||
// </View>
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
|
||||
Reference in New Issue
Block a user