import { useCallback, useEffect, 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 type { ProductListParams } from '@/services/product' import { getProductCover } from '@/types/product' import type { Category, Product } from '@/types/product' import './index.less' const PAGE_SIZE = 10 /** 存储 key:首页点击分类/搜索跳转时经本地存储传参(switchTab 无法带参) */ const PENDING_CATEGORY_KEY = 'product_category_id' const PENDING_KEYWORD_KEY = 'product_keyword' export default function ProductPage() { const addItem = useCartStore(s => s.addItem) /** 分类树 */ const [categories, setCategories] = useState([]) /** 选中的分类ID(null = 全部;仅叶子分类可选:二级分类或无子分类的一级分类) */ const [activeId, setActiveId] = useState(null) /** 展开的一级分类ID(纯 UI 状态:有子分类的一级分类不可选中,点击只展开/收起) */ const [expandedTop, setExpandedTop] = useState(null) /** 已提交的搜索词(onSearch 才生效) */ const [searchKey, setSearchKey] = useState('') /** 输入框内容 */ const [inputKey, setInputKey] = useState('') /** 商品列表 */ const [products, setProducts] = useState([]) const [page, setPage] = useState(1) const [loading, setLoading] = useState(false) const [finished, setFinished] = useState(false) /** 请求序号:并发时仅采用最后一次请求的结果,避免分类快速切换时旧响应覆盖新数据 */ const reqSeqRef = useRef(0) /** 是否有请求进行中(仅用于避免"加载更多"并发) */ const loadingRef = useRef(false) /** 加购弹层 */ const [showPopup, setShowPopup] = useState(false) const [current, setCurrent] = useState(null) const [qty, setQty] = useState(1) const addingRef = useRef(false) /** 当前选中二级分类所属的一级分类ID(用于父级高亮) */ const activeParentId = useMemo(() => { if (activeId == null) return null return categories.find(c => (c.children ?? []).some(ch => ch.id === activeId))?.id ?? null }, [categories, activeId]) /** 当前生效的分类ID */ const effectiveCategoryId = activeId ?? undefined /** 拉取商品列表(keywordOverride 用于状态未更新时显式传本次搜索词) */ const fetchList = useCallback( async (pageNum: number, reset: boolean, keywordOverride?: string) => { if (!reset && loadingRef.current) return const seq = ++reqSeqRef.current loadingRef.current = true setLoading(true) try { // undefined 会被序列化成字符串 "undefined" 拼进 URL,导致后端过滤为空,只传有值参数 const params: ProductListParams = { page: pageNum, pageSize: PAGE_SIZE } if (effectiveCategoryId != null) params.category_id = effectiveCategoryId const keyword = keywordOverride ?? (searchKey || undefined) if (keyword) params.keyword = keyword const res = await getProductListApi(params) if (seq !== reqSeqRef.current) return // 已有更新的请求,丢弃本次响应 const { data, total: totalCount } = res.data setProducts(prev => (reset ? data : [...prev, ...data])) setPage(pageNum) setFinished(pageNum * PAGE_SIZE >= totalCount) } catch { // 错误已由 request 层 toast } finally { if (seq === reqSeqRef.current) { loadingRef.current = false setLoading(false) } } }, [effectiveCategoryId, searchKey], ) /** 分类/搜索词变化时重新加载第一页(首屏由 useDidShow 触发,跳过首次执行) */ const firstEffectRef = useRef(true) useEffect(() => { if (firstEffectRef.current) { firstEffectRef.current = false return } fetchList(1, true) }, [fetchList]) /** 分类树(加载完成后处理首页跳转带入的分类) */ 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 parentOfChild = res.data.find(c => (c.children ?? []).some(ch => ch.id === pending)) const top = res.data.find(c => c.id === pending) if (parentOfChild) { // 带入的是二级分类:展开父级并选中 setExpandedTop(parentOfChild.id) setActiveId(pending) } else if (top) { const children = top.children ?? [] if (children.length > 0) { // 有子分类的一级分类不可选中:展开并默认选中第一个子分类 setExpandedTop(top.id) setActiveId(children[0].id) } else { setActiveId(top.id) } } } } catch { // 错误已由 request 层 toast } }, []) useDidShow(() => { // 处理首页搜索跳转带入的关键词 let pendingKeyword: string | null = null try { pendingKeyword = Taro.getStorageSync(PENDING_KEYWORD_KEY) || null Taro.removeStorageSync(PENDING_KEYWORD_KEY) } catch { // noop } if (pendingKeyword) { setInputKey(pendingKeyword) setSearchKey(pendingKeyword) } loadCategories() fetchList(1, true, pendingKeyword ?? undefined) }) useReachBottom(() => { if (!finished) { fetchList(page + 1, false) } }) /** 点击一级分类:有子分类仅展开/收起(不可选中),无子分类则选中 */ const handleTopTap = useCallback((cat: Category) => { if ((cat.children ?? []).length > 0) { setExpandedTop(prev => (prev === cat.id ? null : cat.id)) return } setActiveId(cat.id) }, []) /** 选中二级分类 */ const handleChildTap = useCallback((id: number) => { setActiveId(id) }, []) /** 选中"全部" */ const handleAllTap = useCallback(() => { setActiveId(null) }, []) /** 提交搜索 */ 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 ( {/* ========== 搜索 ========== */} setInputKey(String(e.detail))} onSearch={handleSearch} onClear={handleClear} /> {/* ========== 左侧分类(一级为分组,叶子分类可选) ========== */} 全部 {categories.map(cat => { const children = cat.children ?? [] const hasChildren = children.length > 0 return ( handleTopTap(cat)} > {cat.name} {hasChildren && ( {expandedTop === cat.id ? '▾' : '▸'} )} {/* 二级分类 */} {expandedTop === cat.id && children.map(child => ( handleChildTap(child.id)} > {child.name} ))} ) })} {/* ========== 右侧商品列表 ========== */} {/* 商品列表 */} {products.length === 0 && !loading ? ( ) : ( products.map(product => ( {product.name} {product.spec} / {product.unit} {product.price !== null ? ( ¥{product.price} ) : ( 价格待定 )} handleAddTap(product)}> )) )} {/* 加载状态 */} {loading && 加载中...} {finished && products.length > 0 && ( 没有更多了 )} {/* ========== 加购弹层 ========== */} setShowPopup(false)} > {current && ( {current.name} {current.spec} / {current.unit} {current.price !== null ? ( ¥{current.price} ) : ( 价格待定 )} 购买数量 setQty(Number(e.detail))} /> )} ) }