From 1729ef3ae4f1ed77362193353dc2d2223504b0e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Sat, 4 Jul 2026 13:17:52 +0800 Subject: [PATCH] =?UTF-8?q?feat(shop):=20=E6=94=B9=E9=80=A0=E5=88=86?= =?UTF-8?q?=E7=B1=BB=E9=A1=B5=E4=B8=BA=E5=B7=A6=E5=8F=B3=E5=B8=83=E5=B1=80?= =?UTF-8?q?=E5=B9=B6=E6=96=B0=E5=A2=9E=E5=95=86=E5=93=81=E6=8E=92=E5=BA=8F?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 重写分类页,实现左侧一级分类列表和右侧商品列表布局 - 右侧增加商品排序栏,支持默认、销量、最新、价格排序(价格可升降序切换) - 商品列表横向卡片布局,显示图片、标题、积分标签、VIP价格、销量及加购按钮 - 支持滚动触底自动分页加载更多商品 - 点击商品跳转详情页,点击加购按钮调用加入购物车接口 - 复用 VIP 价格逻辑,优化价格显示 - 修复门店中心页 loading 状态,确保数据加载完成后隐藏加载中提示 - 优化订阅消息模板 ID 填充和错误提示提示细节 --- .workbuddy/memory/2026-07-04.md | 22 +++ src/pages/shop/category.tsx | 277 +++++++++++++++++++++++++++----- 2 files changed, 260 insertions(+), 39 deletions(-) diff --git a/.workbuddy/memory/2026-07-04.md b/.workbuddy/memory/2026-07-04.md index 00e6d75..e4e738b 100644 --- a/.workbuddy/memory/2026-07-04.md +++ b/.workbuddy/memory/2026-07-04.md @@ -145,3 +145,25 @@ - 新建:`JAVA/shop-api/.../service/WxSubscribeMessageService.java` - 新建:`JAVA/shop-api/.../service/impl/WxSubscribeMessageServiceImpl.java` - 修改:`JAVA/shop-api/.../service/OrderBusinessService.java` + +--- + +### 12. 订阅消息模板 ID 填入 + 错误提示优化 +- 模板 ID:`sh1K9iK7vZjebUNFu6OsMsnsJxm4whThWGrhN7I4zVg`(交易提醒) +- 字段:订单号(`character_string1`)、商品名称(`thing4`)、联系人(`thing10`)、联系电话(`phone_number7`)、送货地址(`thing8`) +- 优化 `requestSubscribeMessage` fail 回调:errCode 20001 提示模板未配置,20002/20004 提示已取消 + +### 13. 门店中心页 loading 状态修复 +- `store/center/index.tsx` 的 `useEffect` 遗漏 `.finally(() => setLoading(false))`,导致页面卡在"加载中" +- 已修复 + +### 14. 分类页改造为左侧分类 + 右侧商品列表布局 +- 重写 `src/pages/shop/category.tsx` +- 左侧:一级分类列表(绿色高亮选中态,保持不变) +- 右侧: + - 排序栏:默认/销量/最新/价格(可切换升降序)/筛选 + - 商品卡片:横向布局(左图 + 右侧标题/积分标签/价格/销量 + 加购按钮) + - 滚动触底分页加载 +- 点击商品跳转详情页,点击加购按钮调用 `addToCart` API +- 复用现有 VIP 价格逻辑(`isVipMember` + `dealerPrice`) +- 编译通过 diff --git a/src/pages/shop/category.tsx b/src/pages/shop/category.tsx index 250f019..f692dbd 100644 --- a/src/pages/shop/category.tsx +++ b/src/pages/shop/category.tsx @@ -1,18 +1,46 @@ -import React, { useState, useEffect } from 'react' +import React, { useState, useEffect, useCallback, useRef } from 'react' import { View, Text, Image, ScrollView } from '@tarojs/components' import Taro from '@tarojs/taro' import { listShopGoodsCategory } from '@/api/shop/shopGoodsCategory' +import { pageShopGoods } from '@/api/shop/shopGoods' +import { addToCart } from '@/api/shop/shopCart' import type { ShopGoodsCategory } from '@/api/shop/shopGoodsCategory/model' +import type { ShopGoods, ShopGoodsParam } from '@/api/shop/shopGoods/model' +import { isGuest } from '@/utils/auth' +import { isVipMember } from '@/utils/vip' +import { requireLogin } from '@/utils/login-guard' +import LoadMore from '@/components/common/LoadMore' import EmptyState from '@/components/common/EmptyState' definePageConfig({ navigationBarTitleText: '商品分类', }) +// ─── 排序选项 ────────────────────────────────────────────────── +type SortKey = 'default' | 'sales' | 'newest' | 'price' + +const SORT_TABS: { key: SortKey; label: string }[] = [ + { key: 'default', label: '默认' }, + { key: 'sales', label: '销量' }, + { key: 'newest', label: '最新' }, + { key: 'price', label: '价格' }, +] + const CategoryPage: React.FC = () => { const [categories, setCategories] = useState([]) const [activeId, setActiveId] = useState() + // 商品列表 + const [goodsList, setGoodsList] = useState([]) + const [loading, setLoading] = useState(false) + const [page, setPage] = useState(1) + const [finished, setFinished] = useState(false) + const loadingRef = useRef(false) + + // 排序 + const [sortKey, setSortKey] = useState('default') + const [priceAsc, setPriceAsc] = useState(true) + useEffect(() => { loadCategories() }, []) @@ -21,30 +49,112 @@ const CategoryPage: React.FC = () => { try { const list = await listShopGoodsCategory() if (list) { - // 过滤:只保留 status=1 且有子分类的分类,避免空壳分类导致审核被拒 - const filtered = list - .map(cat => ({ - ...cat, - children: (cat.children || []).filter(child => child.status === 1), - })) - .filter(cat => cat.status === 1 && cat.children.length > 0) + // 只保留 status=1 的分类(不限是否有子分类,因为右侧直接展示商品) + const filtered = list.filter(cat => cat.status === 1) setCategories(filtered) if (filtered.length > 0) setActiveId(filtered[0].categoryId) } } catch { /* ignore */ } } - const activeCategory = categories.find(c => c.categoryId === activeId) - const children = activeCategory?.children || [] + /** 构建排序参数 */ + const buildSortParams = useCallback((): Partial => { + if (sortKey === 'sales') return { sort: 'sales', order: 'desc' } + if (sortKey === 'newest') return { sort: 'create_time', order: 'desc' } + if (sortKey === 'price') return { sort: 'price', order: priceAsc ? 'asc' : 'desc' } + return {} + }, [sortKey, priceAsc]) + + /** 加载商品列表 */ + const loadGoods = useCallback(async (p: number) => { + if (loadingRef.current || activeId === undefined) return + loadingRef.current = true + setLoading(true) + try { + const params: ShopGoodsParam = { + page: p, + limit: 10, + status: 0, + categoryId: activeId, + ...buildSortParams(), + } + const res = await pageShopGoods(params) + const newList = res?.list || [] + if (p === 1) { + setGoodsList(newList) + } else { + setGoodsList(prev => [...prev, ...newList]) + } + setFinished(newList.length < 10) + setPage(p) + } catch { /* ignore */ } + loadingRef.current = false + setLoading(false) + }, [activeId, buildSortParams]) + + // 分类或排序变化时重新加载 + useEffect(() => { + if (activeId !== undefined) { + setGoodsList([]) + setFinished(false) + setPage(1) + loadGoods(1) + } + }, [activeId, sortKey, priceAsc]) + + const handleLoadMore = () => { + if (!finished && !loading) { + loadGoods(page + 1) + } + } + + /** 排序切换 */ + const handleSortChange = (key: SortKey) => { + if (key === 'price') { + if (sortKey === 'price') { + setPriceAsc(!priceAsc) + } else { + setSortKey('price') + setPriceAsc(true) + } + } else { + setSortKey(key) + } + } + + /** 获取显示价格 */ + const getDisplayPrice = (product: ShopGoods) => { + if (isVipMember() && product.dealerPrice) return product.dealerPrice + return product.price || '0' + } + + /** 加入购物车 */ + const handleAddToCart = (e: any, product: ShopGoods) => { + e.stopPropagation() + if (!requireLogin({ action: 'addToCart' })) return + addToCart({ + goodsId: product.goodsId!, + num: product.step || 1, + }).then(() => { + Taro.showToast({ title: '已加入购物车', icon: 'success' }) + }).catch(err => { + Taro.showToast({ title: err.message || '添加失败', icon: 'none' }) + }) + } + + /** 跳转商品详情 */ + const goDetail = (product: ShopGoods) => { + Taro.navigateTo({ url: `/pages/shop/product-detail?id=${product.goodsId}` }) + } return ( {/* 左侧一级分类 */} - + {categories.map(cat => ( { ))} - {/* 右侧二级分类 */} - - {children.length > 0 ? ( - - - {children.map(child => ( - Taro.switchTab({ url: '/pages/shop/index' })} - > - - {child.image ? ( - - ) : ( - - {(child.title || '')[0]} - - )} - - {child.title} - - ))} + {/* 右侧:排序栏 + 商品列表 */} + + {/* 排序栏 */} + + {SORT_TABS.map(tab => ( + handleSortChange(tab.key)} + > + + {tab.label} + + {tab.key === 'price' && sortKey === 'price' && ( + + {priceAsc ? '↑' : '↓'} + + )} + ))} + + 筛选 - ) : ( - - )} - + + + {/* 商品列表 */} + + + {goodsList.length > 0 ? ( + + {goodsList.map(item => ( + goDetail(item)} + > + {/* 商品图片 */} + + + + + {/* 商品信息 */} + + {/* 标题 + 积分标签 */} + + + {item.name || item.goodsName} + + {item.gainIntegral && item.gainIntegral > 0 ? ( + + + +{item.gainIntegral}积分 + + + ) : null} + + + {/* 价格 + 销量 + 加购 */} + + + + ¥{getDisplayPrice(item)} + + {item.unitName ? ( + /{item.unitName} + ) : null} + {/* 原价划掉 */} + {isVipMember() && item.dealerPrice ? ( + + ¥{item.price} + + ) : item.salePrice && item.salePrice !== item.price ? ( + isGuest() ? null : ( + + ¥{item.salePrice} + + ) + ) : null} + {/* 销量 */} + {item.sales !== undefined && item.sales > 0 && ( + + 已售{item.sales} + + )} + + {/* 加购按钮 */} + handleAddToCart(e, item)} + > + + + + + + + ))} + + ) : ( + !loading && + )} + + + + ) }