修复分类

This commit is contained in:
liu
2026-08-14 16:21:41 +08:00
parent 8d308288bb
commit 4a5b7003ca
4 changed files with 155 additions and 276 deletions
+111 -75
View File
@@ -1,9 +1,10 @@
import { useCallback, useMemo, useRef, useState } from 'react'
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'
@@ -18,10 +19,10 @@ export default function ProductPage() {
/** 分类树 */
const [categories, setCategories] = useState<Category[]>([])
/** 选中的顶级分类(null = 全部) */
const [activeTop, setActiveTop] = useState<number | null>(null)
/** 选中的子分类(null = 顶级分类下全部 */
const [activeChild, setActiveChild] = useState<number | null>(null)
/** 选中的分类IDnull = 全部;仅叶子分类可选:二级分类或无子分类的一级分类 */
const [activeId, setActiveId] = useState<number | null>(null)
/** 展开的一级分类ID(纯 UI 状态:有子分类的一级分类不可选中,点击只展开/收起 */
const [expandedTop, setExpandedTop] = useState<number | null>(null)
/** 已提交的搜索词(onSearch 才生效) */
const [searchKey, setSearchKey] = useState('')
/** 输入框内容 */
@@ -32,7 +33,10 @@ export default function ProductPage() {
const [page, setPage] = useState(1)
const [loading, setLoading] = useState(false)
const [finished, setFinished] = useState(false)
const listLoadingRef = useRef(false)
/** 请求序号:并发时仅采用最后一次请求的结果,避免分类快速切换时旧响应覆盖新数据 */
const reqSeqRef = useRef(0)
/** 是否有请求进行中(仅用于避免"加载更多"并发) */
const loadingRef = useRef(false)
/** 加购弹层 */
const [showPopup, setShowPopup] = useState(false)
@@ -40,31 +44,30 @@ export default function ProductPage() {
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 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 = useMemo(
() => activeChild ?? activeTop ?? undefined,
[activeChild, activeTop],
)
/** 当前生效的分类ID */
const effectiveCategoryId = activeId ?? undefined
/** 拉取商品列表(keywordOverride 用于状态未更新时显式传本次搜索词) */
const fetchList = useCallback(
async (pageNum: number, reset: boolean, keywordOverride?: string) => {
if (listLoadingRef.current) return
listLoadingRef.current = true
if (!reset && loadingRef.current) return
const seq = ++reqSeqRef.current
loadingRef.current = true
setLoading(true)
try {
const res = await getProductListApi({
category_id: effectiveCategoryId,
keyword: keywordOverride ?? (searchKey || undefined),
page: pageNum,
pageSize: PAGE_SIZE,
})
// 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)
@@ -72,13 +75,25 @@ export default function ProductPage() {
} catch {
// 错误已由 request 层 toast
} finally {
listLoadingRef.current = false
setLoading(false)
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 {
@@ -93,14 +108,21 @@ export default function ProductPage() {
// 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)
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)
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 {
@@ -126,20 +148,28 @@ export default function ProductPage() {
})
useReachBottom(() => {
if (!finished && !listLoadingRef.current) {
if (!finished) {
fetchList(page + 1, false)
}
})
/** 切换顶级分类 */
const handleTopTap = useCallback((id: number | null) => {
setActiveTop(id)
setActiveChild(null)
/** 点击一级分类:有子分类仅展开/收起(不可选中),无子分类则选中 */
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 | null) => {
setActiveChild(id)
/** 选中二级分类 */
const handleChildTap = useCallback((id: number) => {
setActiveId(id)
}, [])
/** 选中"全部" */
const handleAllTap = useCallback(() => {
setActiveId(null)
}, [])
/** 提交搜索 */
@@ -183,7 +213,6 @@ export default function ProductPage() {
value={inputKey}
placeholder='搜索品名/规格'
shape='round'
background='#f7f8fa'
onChange={e => setInputKey(String(e.detail))}
onSearch={handleSearch}
onClear={handleClear}
@@ -191,48 +220,55 @@ export default function ProductPage() {
</View>
<View className='product-body'>
{/* ========== 左侧分类 ========== */}
{/* ========== 左侧分类(一级为分组,叶子分类可选) ========== */}
<ScrollView scrollY className='product-categories'>
<View
className={`category-item ${activeTop === null ? 'active' : ''}`}
onClick={() => handleTopTap(null)}
className={`category-item ${activeId === null ? 'active' : ''}`}
onClick={handleAllTap}
>
<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>
))}
{categories.map(cat => {
const children = cat.children ?? []
const hasChildren = children.length > 0
return (
<View key={cat.id}>
<View
className={[
'category-item',
// 有子分类:子分类被选中时父级高亮;无子分类:自身可选中
hasChildren && activeParentId === cat.id && 'expanded',
!hasChildren && activeId === cat.id && 'active',
].filter(Boolean).join(' ')}
onClick={() => handleTopTap(cat)}
>
<Text className='category-item__name'>{cat.name}</Text>
{hasChildren && (
<Text className='category-item__arrow'>
{expandedTop === cat.id ? '▾' : '▸'}
</Text>
)}
</View>
{/* 二级分类 */}
{expandedTop === cat.id &&
children.map(child => (
<View
key={child.id}
className={`category-item category-item--child ${
activeId === child.id ? 'active' : ''
}`}
onClick={() => handleChildTap(child.id)}
>
<Text className='category-item__name'>{child.name}</Text>
</View>
))}
</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' />