Explorar o código

✨ feat(category): 添加商品分类页面

- 创建分类页面配置文件index.config.ts,设置导航栏样式和分享功能
- 实现分类页面主组件,包含一级分类侧边栏和二级分类内容区
- 添加分类数据和广告数据获取逻辑,使用react-query进行数据管理
- 实现分类切换交互功能,支持一级分类和二级分类的切换
- 添加分类项点击跳转商品列表页功能
- 添加加载状态和错误状态处理
- 实现响应式布局,适配移动端显示
yourname hai 1 mes
pai
achega
93c8dcf829

+ 3 - 2
mini/src/app.config.ts

@@ -7,6 +7,7 @@ export default defineAppConfig({
     'pages/login/wechat-login',
     'pages/register/index',
     // 电商相关页面
+    'pages/category/index',
     'pages/goods-list/index',
     'pages/goods-detail/index',
     'pages/cart/index',
@@ -34,8 +35,8 @@ export default defineAppConfig({
         text: '首页'
       },
       {
-        pagePath: 'pages/goods-list/index',
-        text: '商品'
+        pagePath: 'pages/category/index',
+        text: '分类'
       },
       {
         pagePath: 'pages/cart/index',

+ 9 - 0
mini/src/pages/category/index.config.ts

@@ -0,0 +1,9 @@
+export default definePageConfig({
+  navigationBarTitleText: '商品分类',
+  navigationBarBackgroundColor: '#ffffff',
+  navigationBarTextStyle: 'black',
+  enableShareAppMessage: true,
+  enableShareTimeline: true,
+  backgroundColor: '#f5f5f5',
+  backgroundTextStyle: 'light'
+})

+ 159 - 0
mini/src/pages/category/index.css

@@ -0,0 +1,159 @@
+/* 商品分类页面样式 */
+.category-page {
+  height: 100vh;
+  background-color: #f5f5f5;
+}
+
+.category-container {
+  display: flex;
+  height: 100%;
+}
+
+/* 左侧边栏容器 */
+.category-sidebar-container {
+  width: 176rpx;
+  height: 100%;
+  background-color: #f5f5f5;
+}
+
+/* 右侧内容区 */
+.category-content {
+  flex: 1;
+  background-color: white;
+  position: relative;
+  padding: 20rpx;
+}
+
+/* 二级分类标签栏 */
+.sub-category-tabbar {
+  margin-bottom: 20rpx;
+}
+
+/* 二级分类网格布局 */
+.sub-category-grid {
+  display: grid;
+  grid-template-columns: 33.33% 33.33% 33.33%;
+  gap: 20rpx;
+}
+
+.sub-category-item {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 20rpx;
+  border-radius: 16rpx;
+  background-color: #f8f8f8;
+  transition: all 0.3s ease;
+}
+
+.sub-category-item:active {
+  background-color: #e8e8e8;
+  transform: scale(0.95);
+}
+
+.sub-category-image-container {
+  width: 144rpx;
+  height: 144rpx;
+  margin-bottom: 16rpx;
+}
+
+.sub-category-image {
+  width: 100%;
+  height: 100%;
+  border-radius: 16rpx;
+  overflow: hidden;
+  background-color: #e0e0e0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.image-placeholder {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.image-fallback {
+  width: 80rpx;
+  height: 80rpx;
+  border-radius: 50%;
+  background-color: #0071ce;
+  color: white;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 32rpx;
+  font-weight: bold;
+}
+
+.sub-category-name {
+  font-size: 26rpx;
+  color: #333;
+  text-align: center;
+  line-height: 1.4;
+  max-width: 100%;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+/* 广告图容器 */
+.advertisement-container {
+  position: fixed;
+  bottom: 13%;
+  right: 3%;
+  z-index: 100;
+}
+
+.advertisement-image {
+  width: 200rpx;
+  height: 200rpx;
+  border-radius: 16rpx;
+  overflow: hidden;
+  background-color: #fa4126;
+  box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
+}
+
+.ad-image-placeholder {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: white;
+  font-size: 24rpx;
+}
+
+/* 加载和错误状态 */
+.loading, .error {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  height: 200rpx;
+  font-size: 28rpx;
+  color: #666;
+}
+
+.error {
+  color: #fa4126;
+}
+
+/* 响应式适配 */
+@media (max-width: 750rpx) {
+  .category-sidebar-container {
+    width: 160rpx;
+  }
+
+  .sub-category-grid {
+    grid-template-columns: 50% 50%;
+    gap: 16rpx;
+  }
+
+  .sub-category-image-container {
+    width: 120rpx;
+    height: 120rpx;
+  }
+}

+ 198 - 0
mini/src/pages/category/index.tsx

@@ -0,0 +1,198 @@
+import React, { useState } from 'react';
+import { View } from '@tarojs/components';
+import { useQuery } from '@tanstack/react-query';
+import { goodsCategoryClient, advertisementClient } from '@/api';
+import CategorySidebar from '@/components/category/CategorySidebar';
+import CategorySidebarItem from '@/components/category/CategorySidebarItem';
+import CategoryTabbar from '@/components/category/CategoryTabbar';
+import { navigateTo } from '@tarojs/taro';
+import { InferResponseType } from 'hono';
+import './index.css';
+
+type GoodsCategoryResponse = InferResponseType<typeof goodsCategoryClient.$get, 200>
+type Category = GoodsCategoryResponse['data'][0]
+
+type AdvertisementResponse = InferResponseType<typeof advertisementClient.$get, 200>
+type Advertisement = AdvertisementResponse['data'][0]
+
+const CategoryPage: React.FC = () => {
+  const [activeCategoryIndex, setActiveCategoryIndex] = useState<number>(0);
+  const [activeSubCategoryId, setActiveSubCategoryId] = useState<string>('');
+
+  // 获取分类数据
+  const { data: categoryData, isLoading, error } = useQuery({
+    queryKey: ['goods-categories'],
+    queryFn: async () => {
+      const response = await goodsCategoryClient.$get({
+        query: {
+          filters: JSON.stringify({ status: 1 }), // 只显示启用的分类
+          sortBy: 'sort',
+          sortOrder: 'ASC'
+        }
+      });
+      if (response.status !== 200) {
+        throw new Error('获取分类数据失败');
+      }
+      return response.json();
+    },
+    staleTime: 5 * 60 * 1000, // 5分钟缓存
+  });
+
+  // 获取广告数据
+  const { data: advertisementData } = useQuery({
+    queryKey: ['category-advertisements'],
+    queryFn: async () => {
+      const response = await advertisementClient.$get({
+        query: {
+          filters: JSON.stringify({ status: 1, typeId: 2 }), // 过滤启用的分类页广告
+          sortBy: 'sort',
+          sortOrder: 'ASC'
+        }
+      });
+      if (response.status !== 200) {
+        throw new Error('获取广告数据失败');
+      }
+      return response.json();
+    },
+    staleTime: 5 * 60 * 1000,
+  });
+
+  const categories = categoryData?.data || [];
+  const advertisements = advertisementData?.data || [];
+
+  // 当前选中的一级分类
+  const currentCategory = categories[activeCategoryIndex];
+
+  // 当前一级分类的子分类
+  const subCategories = currentCategory?.child_cate || [];
+
+  // 广告图片
+  const advertisementImage = advertisements[0]?.imageFile?.fullUrl || '';
+
+  // 处理一级分类切换
+  const handleCategoryChange = (index: number) => {
+    setActiveCategoryIndex(index);
+    // 重置二级分类选中状态
+    setActiveSubCategoryId('');
+  };
+
+  // 处理二级分类切换
+  const handleSubCategoryChange = (id: string) => {
+    setActiveSubCategoryId(id);
+  };
+
+  // 处理分类跳转
+  const handleCategoryClick = (categoryId: string) => {
+    navigateTo({
+      url: `/pages/goods-list/index?cateId=${encodeURIComponent(categoryId)}`,
+    });
+  };
+
+  // 处理二级分类点击
+  const handleSubCategoryClick = (category: Category) => {
+    handleCategoryClick(String(category.id));
+  };
+
+  if (isLoading) {
+    return (
+      <View className="category-page">
+        <View className="loading">加载中...</View>
+      </View>
+    );
+  }
+
+  if (error) {
+    return (
+      <View className="category-page">
+        <View className="error">加载失败,请重试</View>
+      </View>
+    );
+  }
+
+  return (
+    <View className="category-page">
+      <View className="category-container">
+        {/* 左侧边栏 - 一级分类 */}
+        <View className="category-sidebar-container">
+          <CategorySidebar
+            activeKey={activeCategoryIndex}
+            onChange={handleCategoryChange}
+          >
+            {categories.map((category: Category, index: number) => (
+              <CategorySidebarItem
+                key={category.id}
+                title={category.name}
+                onClick={() => handleCategoryChange(index)}
+              />
+            ))}
+          </CategorySidebar>
+        </View>
+
+        {/* 右侧内容区 */}
+        <View className="category-content">
+          {/* 二级分类标签栏 */}
+          {subCategories.length > 0 && (
+            <View className="sub-category-tabbar">
+              <CategoryTabbar
+                tabList={subCategories.map((cat: Category) => ({
+                  id: cat.id,
+                  name: cat.name,
+                }))}
+                currentActive={activeSubCategoryId}
+                onChange={handleSubCategoryChange}
+                showMore={true}
+              />
+            </View>
+          )}
+
+          {/* 二级分类网格布局 */}
+          <View className="sub-category-grid">
+            {subCategories.map((category: Category) => (
+              <View
+                key={category.id}
+                className="sub-category-item"
+                onClick={() => handleSubCategoryClick(category)}
+              >
+                <View className="sub-category-image-container">
+                  <View className="sub-category-image">
+                    {category.image ? (
+                      <View className="image-placeholder">
+                        {/* 这里需要集成 Shadcn Image 组件 */}
+                        <View className="image-fallback">
+                          {category.name.charAt(0)}
+                        </View>
+                      </View>
+                    ) : (
+                      <View className="image-placeholder">
+                        <View className="image-fallback">
+                          {category.name.charAt(0)}
+                        </View>
+                      </View>
+                    )}
+                  </View>
+                </View>
+                <View className="sub-category-name">
+                  {category.name}
+                </View>
+              </View>
+            ))}
+          </View>
+
+          {/* 广告图 */}
+          {advertisementImage && (
+            <View className="advertisement-container">
+              <View className="advertisement-image">
+                {/* 这里需要集成 Shadcn Image 组件 */}
+                <View className="ad-image-placeholder">
+                  广告图
+                </View>
+              </View>
+            </View>
+          )}
+        </View>
+      </View>
+    </View>
+  );
+};
+
+export default CategoryPage;