Преглед на файлове

✨ feat(goods-category-management-ui-mt): 添加商品分类树形管理组件

- 复制区域管理组件并修改为商品分类树形管理
- 新增 GoodsCategoryTreeManagement 组件支持异步加载树形结构
- 新增 GoodsCategoryTreeAsync 组件实现树形节点渲染
- 新增 GoodsCategoryForm 组件提供分类表单
- 更新 GoodsCategorySelector 使用客户端管理器
- 扩展类型定义支持树形节点结构
- 更新组件导出文件包含新组件

🤖 Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
yourname преди 1 месец
родител
ревизия
2aee71229f

+ 228 - 0
packages/goods-category-management-ui-mt/src/components/GoodsCategoryForm.tsx

@@ -0,0 +1,228 @@
+import React from 'react';
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { Button } from '@d8d/shared-ui-components/components/ui/button';
+import { Input } from '@d8d/shared-ui-components/components/ui/input';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@d8d/shared-ui-components/components/ui/select';
+import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@d8d/shared-ui-components/components/ui/form';
+import { FileSelector } from '@d8d/file-management-ui-mt';
+import { CreateGoodsCategoryDto, UpdateGoodsCategoryDto } from '@d8d/goods-module-mt/schemas';
+import type { CreateGoodsCategoryDto as CreateGoodsCategoryInput, UpdateGoodsCategoryDto as UpdateGoodsCategoryInput } from '@d8d/goods-module-mt/schemas';
+
+// 状态枚举
+enum CategoryState {
+  ENABLED = 1,
+  DISABLED = 2
+}
+
+interface GoodsCategoryFormProps {
+  category?: UpdateGoodsCategoryInput & { id?: number };
+  onSubmit: (data: CreateGoodsCategoryInput | UpdateGoodsCategoryInput) => Promise<void>;
+  onCancel: () => void;
+  isLoading?: boolean;
+  /** 智能预填的层级 */
+  smartLevel?: number;
+  /** 智能预填的父级ID */
+  smartParentId?: number;
+}
+
+// 辅助函数:根据层级值获取显示名称
+const getLevelDisplayName = (level: number | undefined): string => {
+  switch (level) {
+    case 0:
+      return '顶级分类';
+    case 1:
+      return '一级分类';
+    case 2:
+      return '二级分类';
+    case 3:
+      return '三级分类';
+    case 4:
+      return '四级分类';
+    default:
+      return `${level}级分类`;
+  }
+};
+
+export const GoodsCategoryForm: React.FC<GoodsCategoryFormProps> = ({
+  category,
+  onSubmit,
+  onCancel,
+  isLoading = false,
+  smartLevel,
+  smartParentId
+}) => {
+  const isEditing = !!category;
+
+  const form = useForm<CreateGoodsCategoryInput | UpdateGoodsCategoryInput>({
+    resolver: zodResolver(isEditing ? UpdateGoodsCategoryDto : CreateGoodsCategoryDto),
+    defaultValues: category ? {
+      tenantId: category.tenantId,
+      parentId: category.parentId,
+      name: category.name,
+      level: category.level,
+      state: category.state,
+      imageFileId: category.imageFileId,
+    } : {
+      tenantId: 1, // 测试环境使用默认tenantId
+      parentId: smartParentId || 0,
+      name: '',
+      level: smartLevel ?? 0,
+      state: CategoryState.ENABLED,
+      imageFileId: null,
+    },
+  });
+
+  const handleSubmit = async (data: CreateGoodsCategoryInput | UpdateGoodsCategoryInput) => {
+    await onSubmit(data);
+  };
+
+  return (
+    <Form {...form}>
+      <form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6">
+        <div className="grid grid-cols-1 gap-6">
+          {/* 层级显示(只读) */}
+          <FormField
+            control={form.control}
+            name="level"
+            render={({ field }) => (
+              <FormItem>
+                <FormLabel>层级</FormLabel>
+                <FormControl>
+                  <Input
+                    value={getLevelDisplayName(field.value)}
+                    disabled
+                    className="bg-muted"
+                  />
+                </FormControl>
+                <FormDescription>
+                  根据操作上下文自动设置的层级
+                </FormDescription>
+                <FormMessage />
+              </FormItem>
+            )}
+          />
+
+          {/* 父级分类显示(只读) */}
+          <FormField
+            control={form.control}
+            name="parentId"
+            render={({ field }) => (
+              <FormItem>
+                <FormLabel>父级分类</FormLabel>
+                <FormControl>
+                  <Input
+                    type="number"
+                    value={field.value || ''}
+                    disabled
+                    className="bg-muted"
+                    placeholder="顶级分类(无父级)"
+                  />
+                </FormControl>
+                <FormDescription>
+                  根据操作上下文自动设置的父级分类ID
+                </FormDescription>
+                <FormMessage />
+              </FormItem>
+            )}
+          />
+
+          {/* 分类名称 */}
+          <FormField
+            control={form.control}
+            name="name"
+            render={({ field }) => (
+              <FormItem>
+                <FormLabel>分类名称</FormLabel>
+                <FormControl>
+                  <Input
+                    placeholder="输入分类名称"
+                    {...field}
+                  />
+                </FormControl>
+                <FormDescription>
+                  输入商品分类名称,如:电子产品、服装、食品等
+                </FormDescription>
+                <FormMessage />
+              </FormItem>
+            )}
+          />
+
+          {/* 状态选择 */}
+          <FormField
+            control={form.control}
+            name="state"
+            render={({ field }) => (
+              <FormItem>
+                <FormLabel>状态</FormLabel>
+                <Select onValueChange={(value) => field.onChange(Number(value))} defaultValue={field.value?.toString()}>
+                  <FormControl>
+                    <SelectTrigger>
+                      <SelectValue placeholder="选择状态" />
+                    </SelectTrigger>
+                  </FormControl>
+                  <SelectContent>
+                    <SelectItem value={CategoryState.ENABLED.toString()}>
+                      启用
+                    </SelectItem>
+                    <SelectItem value={CategoryState.DISABLED.toString()}>
+                      禁用
+                    </SelectItem>
+                  </SelectContent>
+                </Select>
+                <FormDescription>
+                  选择商品分类状态
+                </FormDescription>
+                <FormMessage />
+              </FormItem>
+            )}
+          />
+
+          {/* 分类图片 */}
+          <FormField
+            control={form.control}
+            name="imageFileId"
+            render={({ field }) => (
+              <FormItem>
+                <FormLabel>分类图片</FormLabel>
+                <FormControl>
+                  <FileSelector
+                    value={field.value || undefined}
+                    onChange={(value) => field.onChange(value)}
+                    maxSize={2}
+                    uploadPath="/goods-categories"
+                    previewSize="medium"
+                    placeholder="选择分类图片"
+                    filterType="image"
+                  />
+                </FormControl>
+                <FormDescription>
+                  上传商品分类的展示图片
+                </FormDescription>
+                <FormMessage />
+              </FormItem>
+            )}
+          />
+        </div>
+
+        {/* 表单操作按钮 */}
+        <div className="flex justify-end gap-4">
+          <Button
+            type="button"
+            variant="outline"
+            onClick={onCancel}
+            disabled={isLoading}
+          >
+            取消
+          </Button>
+          <Button
+            type="submit"
+            disabled={isLoading}
+          >
+            {isLoading ? '提交中...' : isEditing ? '更新' : '创建'}
+          </Button>
+        </div>
+      </form>
+    </Form>
+  );
+};

+ 2 - 2
packages/goods-category-management-ui-mt/src/components/GoodsCategorySelector.tsx

@@ -7,7 +7,7 @@ import {
   SelectTrigger,
   SelectValue,
 } from '@d8d/shared-ui-components/components/ui/select';
-import { goodsCategoryClient } from '../api/goodsCategoryClient';
+import { goodsCategoryClientManager } from '../api/goodsCategoryClient';
 
 interface GoodsCategorySelectorProps {
   value?: number;
@@ -29,7 +29,7 @@ const GoodsCategorySelector: React.FC<GoodsCategorySelectorProps> = ({
   const { data: categories, isLoading } = useQuery({
     queryKey: ['goods-categories', level, parentId],
     queryFn: async () => {
-      const res = await goodsCategoryClient.$get({
+      const res = await goodsCategoryClientManager.get().index.$get({
         query: {
           page: 1,
           pageSize: 100,

+ 306 - 0
packages/goods-category-management-ui-mt/src/components/GoodsCategoryTreeAsync.tsx

@@ -0,0 +1,306 @@
+import React from 'react';
+import { ChevronRight, ChevronDown, Folder, FolderOpen, Loader2 } from 'lucide-react';
+import { Button } from '@d8d/shared-ui-components/components/ui/button';
+import { Badge } from '@d8d/shared-ui-components/components/ui/badge';
+import { cn } from '@d8d/shared-ui-components/utils';
+import { useQuery } from '@tanstack/react-query';
+import { goodsCategoryClientManager } from '../api/goodsCategoryClient';
+import type { GoodsCategoryNode } from '../types/goodsCategory';
+
+interface GoodsCategoryTreeAsyncProps {
+  categories: GoodsCategoryNode[];
+  expandedNodes: Set<number>;
+  onToggleNode: (nodeId: number) => void;
+  onEdit: (category: GoodsCategoryNode) => void;
+  onDelete: (category: GoodsCategoryNode) => void;
+  onToggleStatus: (category: GoodsCategoryNode) => void;
+  onAddChild: (category: GoodsCategoryNode) => void;
+}
+
+// 子树加载组件
+interface SubTreeLoaderProps {
+  nodeId: number;
+  isExpanded: boolean;
+  hasChildren: boolean;
+  depth: number;
+  expandedNodes: Set<number>;
+  onToggleNode: (nodeId: number) => void;
+  onEdit: (category: GoodsCategoryNode) => void;
+  onDelete: (category: GoodsCategoryNode) => void;
+  onToggleStatus: (category: GoodsCategoryNode) => void;
+  onAddChild: (category: GoodsCategoryNode) => void;
+}
+
+const SubTreeLoader: React.FC<SubTreeLoaderProps> = ({
+  nodeId,
+  isExpanded,
+  hasChildren,
+  depth,
+  expandedNodes,
+  onToggleNode,
+  onEdit,
+  onDelete,
+  onToggleStatus,
+  onAddChild
+}) => {
+  const { data: subTreeData, isLoading: isSubTreeLoading } = useQuery({
+    queryKey: ['goods-categories-subtree', nodeId],
+    queryFn: async () => {
+      const res = await goodsCategoryClientManager.get().index.$get({
+        query: {
+          page: 1,
+          pageSize: 100,
+          filters: JSON.stringify({ parentId: nodeId }),
+          sortBy: 'id',
+          sortOrder: 'ASC'
+        }
+      });
+      if (res.status !== 200) throw new Error('获取子树失败');
+      const response = await res.json();
+      return response.data;
+    },
+    enabled: isExpanded && hasChildren,
+    staleTime: 5 * 60 * 1000,
+    gcTime: 10 * 60 * 1000,
+  });
+
+  if (isSubTreeLoading) {
+    return (
+      <div className="flex items-center justify-center py-2 px-3 text-muted-foreground">
+        <Loader2 className="h-4 w-4 animate-spin mr-2" />
+        加载中...
+      </div>
+    );
+  }
+
+  if (!subTreeData) {
+    return (
+      <div className="py-2 px-3 text-muted-foreground text-sm">
+        暂无子节点
+      </div>
+    );
+  }
+
+  // subTreeData 是一个 GoodsCategoryNode 数组,直接使用
+  const childNodes = subTreeData || [];
+
+  if (childNodes.length === 0) {
+    return (
+      <div className="py-2 px-3 text-muted-foreground text-sm">
+        暂无子节点
+      </div>
+    );
+  }
+
+  return (
+    <div>
+      {childNodes.map((node: GoodsCategoryNode) => (
+        <TreeNode
+          key={node.id}
+          node={node}
+          depth={depth + 1}
+          expandedNodes={expandedNodes}
+          onToggleNode={onToggleNode}
+          onEdit={onEdit}
+          onDelete={onDelete}
+          onToggleStatus={onToggleStatus}
+          onAddChild={onAddChild}
+        />
+      ))}
+    </div>
+  );
+};
+
+// 树节点组件
+interface TreeNodeProps {
+  node: GoodsCategoryNode;
+  depth?: number;
+  expandedNodes: Set<number>;
+  onToggleNode: (nodeId: number) => void;
+  onEdit: (category: GoodsCategoryNode) => void;
+  onDelete: (category: GoodsCategoryNode) => void;
+  onToggleStatus: (category: GoodsCategoryNode) => void;
+  onAddChild: (category: GoodsCategoryNode) => void;
+}
+
+const TreeNode: React.FC<TreeNodeProps> = ({
+  node,
+  depth = 0,
+  expandedNodes,
+  onToggleNode,
+  onEdit,
+  onDelete,
+  onToggleStatus,
+  onAddChild
+}) => {
+  const isExpanded = expandedNodes.has(node.id);
+  const isDisabled = node.state === 2;
+  const hasChildren = true; // 商品分类理论上可以无限层级
+
+  return (
+    <div key={node.id} className="select-none">
+      {/* 节点行 */}
+      <div
+        className={cn(
+          "group flex items-center gap-2 py-2 px-3 hover:bg-muted/50 cursor-pointer border-b",
+          depth > 0 && "ml-6"
+        )}
+        style={{ marginLeft: `${depth * 24}px` }}
+      >
+        {/* 展开/收起按钮 */}
+        {hasChildren && (
+          <Button
+            variant="ghost"
+            size="sm"
+            className="h-6 w-6 p-0"
+            onClick={() => onToggleNode(node.id)}
+          >
+            {isExpanded ? (
+              <ChevronDown className="h-4 w-4" />
+            ) : (
+              <ChevronRight className="h-4 w-4" />
+            )}
+          </Button>
+        )}
+        {!hasChildren && <div className="w-6" />}
+
+        {/* 图标 */}
+        <div className="flex-shrink-0">
+          {hasChildren ? (
+            isExpanded ? (
+              <FolderOpen className="h-4 w-4 text-blue-500" />
+            ) : (
+              <Folder className="h-4 w-4 text-blue-400" />
+            )
+          ) : (
+            <div className="h-4 w-4" />
+          )}
+        </div>
+
+        {/* 节点信息 */}
+        <div className="flex-1 flex items-center gap-3">
+          <span className={cn("font-medium", isDisabled && "text-muted-foreground line-through")}>
+            {node.name}
+          </span>
+          <Badge variant="outline" className="text-xs">
+            {getLevelName(node.level)}
+          </Badge>
+          {node.imageFile?.fullUrl && (
+            <img
+              src={node.imageFile.fullUrl}
+              alt={node.name}
+              className="w-8 h-8 object-cover rounded"
+              onError={(e) => {
+                e.currentTarget.src = '/placeholder.png';
+              }}
+            />
+          )}
+          <Badge variant={isDisabled ? "secondary" : "default"} className="text-xs">
+            {isDisabled ? '禁用' : '启用'}
+          </Badge>
+        </div>
+
+        {/* 操作按钮 */}
+        <div className="flex gap-1 opacity-100 transition-opacity">
+          {/* 新增子节点按钮 */}
+          <Button
+            variant="outline"
+            size="sm"
+            onClick={(e) => {
+              e.stopPropagation();
+              onAddChild(node);
+            }}
+          >
+            新增子分类
+          </Button>
+          <Button
+            variant="outline"
+            size="sm"
+            onClick={(e) => {
+              e.stopPropagation();
+              onEdit(node);
+            }}
+          >
+            编辑
+          </Button>
+          <Button
+            variant="outline"
+            size="sm"
+            onClick={(e) => {
+              e.stopPropagation();
+              onToggleStatus(node);
+            }}
+          >
+            {isDisabled ? '启用' : '禁用'}
+          </Button>
+          <Button
+            variant="outline"
+            size="sm"
+            onClick={(e) => {
+              e.stopPropagation();
+              onDelete(node);
+            }}
+          >
+            删除
+          </Button>
+        </div>
+      </div>
+
+      {/* 子节点 */}
+      {isExpanded && hasChildren && (
+        <SubTreeLoader
+          nodeId={node.id}
+          isExpanded={isExpanded}
+          hasChildren={hasChildren}
+          depth={depth}
+          expandedNodes={expandedNodes}
+          onToggleNode={onToggleNode}
+          onEdit={onEdit}
+          onDelete={onDelete}
+          onToggleStatus={onToggleStatus}
+          onAddChild={onAddChild}
+        />
+      )}
+    </div>
+  );
+};
+
+export const GoodsCategoryTreeAsync: React.FC<GoodsCategoryTreeAsyncProps> = ({
+  categories,
+  expandedNodes,
+  onToggleNode,
+  onEdit,
+  onDelete,
+  onToggleStatus,
+  onAddChild
+}) => {
+  return (
+    <div className="border rounded-lg bg-background">
+      {categories.map(category => (
+        <TreeNode
+          key={category.id}
+          node={category}
+          depth={0}
+          expandedNodes={expandedNodes}
+          onToggleNode={onToggleNode}
+          onEdit={onEdit}
+          onDelete={onDelete}
+          onToggleStatus={onToggleStatus}
+          onAddChild={onAddChild}
+        />
+      ))}
+    </div>
+  );
+};
+
+// 获取层级显示名称
+const getLevelName = (level: number) => {
+  switch (level) {
+    case 0: return '顶级分类';
+    case 1: return '一级分类';
+    case 2: return '二级分类';
+    case 3: return '三级分类';
+    case 4: return '四级分类';
+    default: return `${level}级分类`;
+  }
+};

+ 454 - 0
packages/goods-category-management-ui-mt/src/components/GoodsCategoryTreeManagement.tsx

@@ -0,0 +1,454 @@
+import React from 'react';
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { Button } from '@d8d/shared-ui-components/components/ui/button';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@d8d/shared-ui-components/components/ui/card';
+import { Plus } from 'lucide-react';
+import { useState } from 'react';
+import { goodsCategoryClient, goodsCategoryClientManager } from '../api/goodsCategoryClient';
+import type { InferResponseType, InferRequestType } from 'hono/client';
+import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@d8d/shared-ui-components/components/ui/dialog';
+import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@d8d/shared-ui-components/components/ui/alert-dialog';
+import { GoodsCategoryForm } from './GoodsCategoryForm';
+import { GoodsCategoryTreeAsync } from './GoodsCategoryTreeAsync';
+import type { CreateGoodsCategoryDto, UpdateGoodsCategoryDto } from '@d8d/goods-module-mt/schemas';
+import type { GoodsCategoryNode } from '../types/goodsCategory';
+import { toast } from 'sonner';
+
+// 类型提取规范
+type GoodsCategoryResponse = InferResponseType<typeof goodsCategoryClient.index.$get, 200>['data'][0];
+type CreateGoodsCategoryRequest = InferRequestType<typeof goodsCategoryClient.index.$post>['json'];
+type UpdateGoodsCategoryRequest = InferRequestType<typeof goodsCategoryClient[':id']['$put']>['json'];
+
+
+// 统一操作处理函数
+const handleOperation = async (operation: () => Promise<void>) => {
+  try {
+    await operation();
+  } catch (error) {
+    throw error;
+  }
+};
+
+export const GoodsCategoryTreeManagement: React.FC = () => {
+  const queryClient = useQueryClient();
+  const [expandedNodes, setExpandedNodes] = useState<Set<number>>(new Set());
+  const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
+  const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
+  const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
+  const [isStatusDialogOpen, setIsStatusDialogOpen] = useState(false);
+  const [selectedCategory, setSelectedCategory] = useState<GoodsCategoryResponse | null>(null);
+  const [isAddChildDialogOpen, setIsAddChildDialogOpen] = useState(false);
+  const [parentCategoryForChild, setParentCategoryForChild] = useState<GoodsCategoryNode | null>(null);
+
+  // 查询顶级分类数据(异步加载)
+  const { data: topLevelData, isLoading: isTopLevelLoading } = useQuery({
+    queryKey: ['goods-categories-tree-top'],
+    queryFn: async () => {
+      const res = await goodsCategoryClientManager.get().index.$get({
+        query: {
+          page: 1,
+          pageSize: 100,
+          filters: JSON.stringify({ level: 0 }),
+          sortBy: 'id',
+          sortOrder: 'ASC'
+        }
+      });
+      if (res.status !== 200) throw new Error('获取顶级分类数据失败');
+      const response = await res.json();
+      return response.data;
+    },
+    staleTime: 5 * 60 * 1000,
+    gcTime: 10 * 60 * 1000,
+  });
+
+  // 创建商品分类
+  const createMutation = useMutation({
+    mutationFn: async (data: CreateGoodsCategoryRequest) => {
+      await handleOperation(async () => {
+        const res = await goodsCategoryClientManager.get().index.$post({ json: data });
+        if (res.status !== 201) throw new Error('创建商品分类失败');
+      });
+    },
+    onSuccess: (_, variables) => {
+      // 更新根级缓存
+      queryClient.invalidateQueries({ queryKey: ['goods-categories-tree-top'] });
+
+      // 如果创建的是子节点,更新父节点的子树缓存
+      if (variables.parentId) {
+        queryClient.invalidateQueries({ queryKey: ['goods-categories-subtree', variables.parentId] });
+      }
+
+      // 显示成功提示
+      toast.success('商品分类创建成功');
+
+      // 关闭对话框
+      setIsCreateDialogOpen(false);
+
+      // 如果是创建子节点,还需要关闭子节点对话框
+      if (variables.parentId) {
+        setIsAddChildDialogOpen(false);
+        setParentCategoryForChild(null);
+      }
+    },
+    onError: () => {
+      toast.error('创建失败,请重试');
+    }
+  });
+
+  // 更新商品分类
+  const updateMutation = useMutation({
+    mutationFn: async ({ id, data }: { id: number; data: UpdateGoodsCategoryRequest }) => {
+      await handleOperation(async () => {
+        const res = await goodsCategoryClientManager.get()[':id'].$put({
+          param: { id },
+          json: data
+        });
+        if (res.status !== 200) throw new Error('更新商品分类失败');
+      });
+    },
+    onSuccess: () => {
+      // 更新根级缓存
+      queryClient.invalidateQueries({ queryKey: ['goods-categories-tree-top'] });
+
+      // 如果更新的节点有父节点,更新父节点的子树缓存
+      if (selectedCategory?.parentId) {
+        queryClient.invalidateQueries({ queryKey: ['goods-categories-subtree', selectedCategory.parentId] });
+      }
+
+      // 显示成功提示
+      toast.success('商品分类更新成功');
+
+      setIsEditDialogOpen(false);
+      setSelectedCategory(null);
+    },
+    onError: () => {
+      toast.error('更新失败,请重试');
+    }
+  });
+
+  // 删除商品分类
+  const deleteMutation = useMutation({
+    mutationFn: async (id: number) => {
+      await handleOperation(async () => {
+        const res = await goodsCategoryClientManager.get()[':id'].$delete({
+          param: { id }
+        });
+        if (res.status !== 204) throw new Error('删除商品分类失败');
+      });
+    },
+    onSuccess: () => {
+      // 更新根级缓存
+      queryClient.invalidateQueries({ queryKey: ['goods-categories-tree-top'] });
+
+      // 如果删除的节点有父节点,更新父节点的子树缓存
+      if (selectedCategory?.parentId) {
+        queryClient.invalidateQueries({ queryKey: ['goods-categories-subtree', selectedCategory.parentId] });
+      }
+
+      // 显示成功提示
+      toast.success('商品分类删除成功');
+
+      setIsDeleteDialogOpen(false);
+      setSelectedCategory(null);
+    },
+    onError: () => {
+      toast.error('删除失败,请重试');
+    }
+  });
+
+  // 启用/禁用商品分类
+  const toggleStatusMutation = useMutation({
+    mutationFn: async ({ id, state }: { id: number; state: number }) => {
+      await handleOperation(async () => {
+        const res = await goodsCategoryClientManager.get()[':id'].$put({
+          param: { id },
+          json: { state }
+        });
+        if (res.status !== 200) throw new Error('更新商品分类状态失败');
+      });
+    },
+    onSuccess: () => {
+      // 更新根级缓存
+      queryClient.invalidateQueries({ queryKey: ['goods-categories-tree-top'] });
+
+      // 如果状态切换的节点有父节点,更新父节点的子树缓存
+      if (selectedCategory?.parentId) {
+        queryClient.invalidateQueries({ queryKey: ['goods-categories-subtree', selectedCategory.parentId] });
+      }
+
+      // 显示成功提示
+      toast.success(`商品分类${selectedCategory?.state === 1 ? '禁用' : '启用'}成功`);
+
+      setIsStatusDialogOpen(false);
+      setSelectedCategory(null);
+    },
+    onError: () => {
+      toast.error('状态切换失败,请重试');
+    }
+  });
+
+  // 处理创建商品分类
+  const handleCreateCategory = async (data: CreateGoodsCategoryDto | UpdateGoodsCategoryDto) => {
+    const createData = {
+      ...data,
+      tenantId: 1 // 测试环境使用默认tenantId
+    } as CreateGoodsCategoryDto;
+    await createMutation.mutateAsync(createData);
+  };
+
+  // 处理更新商品分类
+  const handleUpdateCategory = async (data: UpdateGoodsCategoryDto) => {
+    if (!selectedCategory) return;
+    const updateData = {
+      ...data,
+      tenantId: 1 // 测试环境使用默认tenantId
+    } as UpdateGoodsCategoryDto;
+    await updateMutation.mutateAsync({ id: selectedCategory.id, data: updateData });
+  };
+
+  // 处理删除商品分类
+  const handleDeleteCategory = async () => {
+    if (!selectedCategory) return;
+    await deleteMutation.mutateAsync(selectedCategory.id);
+  };
+
+  // 处理启用/禁用商品分类
+  const handleToggleStatus = async (state: number) => {
+    if (!selectedCategory) return;
+    await toggleStatusMutation.mutateAsync({ id: selectedCategory.id, state });
+  };
+
+  // 处理新增子节点
+  const handleAddChild = (category: GoodsCategoryNode) => {
+    setParentCategoryForChild(category);
+    setIsAddChildDialogOpen(true);
+  };
+
+  // 处理创建子节点
+  const handleCreateChildCategory = async (data: CreateGoodsCategoryDto | UpdateGoodsCategoryDto) => {
+    const createData = {
+      ...data,
+      tenantId: 1 // 测试环境使用默认tenantId
+    } as CreateGoodsCategoryDto;
+    await createMutation.mutateAsync(createData);
+  };
+
+  // 打开编辑对话框
+  const handleEdit = (category: GoodsCategoryNode) => {
+    // 将 GoodsCategoryNode 转换为 GoodsCategoryResponse
+    const categoryResponse: GoodsCategoryResponse = {
+      ...category,
+      createdAt: new Date().toISOString(),
+      updatedAt: new Date().toISOString(),
+      createdBy: null,
+      updatedBy: null
+    };
+    setSelectedCategory(categoryResponse);
+    setIsEditDialogOpen(true);
+  };
+
+  // 打开删除对话框
+  const handleDelete = (category: GoodsCategoryNode) => {
+    // 将 GoodsCategoryNode 转换为 GoodsCategoryResponse
+    const categoryResponse: GoodsCategoryResponse = {
+      ...category,
+      createdAt: new Date().toISOString(),
+      updatedAt: new Date().toISOString(),
+      createdBy: null,
+      updatedBy: null
+    };
+    setSelectedCategory(categoryResponse);
+    setIsDeleteDialogOpen(true);
+  };
+
+  // 打开状态切换对话框
+  const handleToggleStatusDialog = (category: GoodsCategoryNode) => {
+    // 将 GoodsCategoryNode 转换为 GoodsCategoryResponse
+    const categoryResponse: GoodsCategoryResponse = {
+      ...category,
+      createdAt: new Date().toISOString(),
+      updatedAt: new Date().toISOString(),
+      createdBy: null,
+      updatedBy: null
+    };
+    setSelectedCategory(categoryResponse);
+    setIsStatusDialogOpen(true);
+  };
+
+  // 切换节点展开状态
+  const handleToggleNode = (nodeId: number) => {
+    setExpandedNodes(prev => {
+      const newSet = new Set(prev);
+      if (newSet.has(nodeId)) {
+        newSet.delete(nodeId);
+      } else {
+        newSet.add(nodeId);
+      }
+      return newSet;
+    });
+  };
+
+  return (
+    <div className="space-y-6">
+      <div className="flex items-center justify-between">
+        <div>
+          <h1 className="text-3xl font-bold tracking-tight">商品分类树形管理</h1>
+          <p className="text-muted-foreground">
+            异步加载树形结构,高效管理商品分类数据
+          </p>
+        </div>
+        <Button onClick={() => setIsCreateDialogOpen(true)}>
+          <Plus className="mr-2 h-4 w-4" />
+          新增顶级分类
+        </Button>
+      </div>
+
+      <Card>
+        <CardHeader>
+          <CardTitle>商品分类树形结构</CardTitle>
+          <CardDescription>
+            以树形结构查看和管理商品分类层级关系,默认只加载顶级分类数据
+          </CardDescription>
+        </CardHeader>
+        <CardContent>
+          {/* 树形视图 */}
+          {isTopLevelLoading ? (
+            <div className="text-center py-8">
+              加载中...
+            </div>
+          ) : !topLevelData || topLevelData.length === 0 ? (
+            <div className="text-center py-8">
+              暂无数据
+            </div>
+          ) : (
+            <GoodsCategoryTreeAsync
+              categories={topLevelData}
+              expandedNodes={expandedNodes}
+              onToggleNode={handleToggleNode}
+              onEdit={handleEdit}
+              onDelete={handleDelete}
+              onToggleStatus={handleToggleStatusDialog}
+              onAddChild={handleAddChild}
+            />
+          )}
+        </CardContent>
+      </Card>
+
+      {/* 创建商品分类对话框 */}
+      <Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
+        <DialogContent className="max-w-2xl">
+          <DialogHeader>
+            <DialogTitle>新增顶级分类</DialogTitle>
+            <DialogDescription>
+              填写顶级分类信息
+            </DialogDescription>
+          </DialogHeader>
+          <GoodsCategoryForm
+            onSubmit={handleCreateCategory}
+            isLoading={createMutation.isPending}
+            onCancel={() => setIsCreateDialogOpen(false)}
+            smartLevel={0} // 默认设置为顶级分类
+          />
+        </DialogContent>
+      </Dialog>
+
+      {/* 编辑商品分类对话框 */}
+      <Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
+        <DialogContent className="max-w-2xl">
+          <DialogHeader>
+            <DialogTitle>编辑商品分类</DialogTitle>
+            <DialogDescription>
+              修改商品分类信息
+            </DialogDescription>
+          </DialogHeader>
+          {selectedCategory && (
+            <GoodsCategoryForm
+              category={{
+                id: selectedCategory.id,
+                parentId: selectedCategory.parentId || 0,
+                name: selectedCategory.name,
+                level: selectedCategory.level,
+                state: selectedCategory.state,
+                imageFileId: selectedCategory.imageFileId
+              }}
+              onSubmit={handleUpdateCategory}
+              isLoading={updateMutation.isPending}
+              onCancel={() => {
+                setIsEditDialogOpen(false);
+                setSelectedCategory(null);
+              }}
+            />
+          )}
+        </DialogContent>
+      </Dialog>
+
+      {/* 新增子节点对话框 */}
+      <Dialog open={isAddChildDialogOpen} onOpenChange={setIsAddChildDialogOpen}>
+        <DialogContent className="max-w-2xl">
+          <DialogHeader>
+            <DialogTitle>
+              新增子分类
+            </DialogTitle>
+            <DialogDescription>
+              在分类 "{parentCategoryForChild?.name}" 下新增子分类
+            </DialogDescription>
+          </DialogHeader>
+          <GoodsCategoryForm
+            onSubmit={handleCreateChildCategory}
+            isLoading={createMutation.isPending}
+            onCancel={() => {
+              setIsAddChildDialogOpen(false);
+              setParentCategoryForChild(null);
+            }}
+            smartLevel={(parentCategoryForChild?.level ?? 0) + 1}
+            smartParentId={parentCategoryForChild?.id}
+          />
+        </DialogContent>
+      </Dialog>
+
+      {/* 删除确认对话框 */}
+      <AlertDialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
+        <AlertDialogContent>
+          <AlertDialogHeader>
+            <AlertDialogTitle>确认删除</AlertDialogTitle>
+            <AlertDialogDescription>
+              确定要删除商品分类 "{selectedCategory?.name}" 吗?此操作不可恢复。
+            </AlertDialogDescription>
+          </AlertDialogHeader>
+          <AlertDialogFooter>
+            <AlertDialogCancel>取消</AlertDialogCancel>
+            <AlertDialogAction
+              onClick={handleDeleteCategory}
+              disabled={deleteMutation.isPending}
+            >
+              {deleteMutation.isPending ? '删除中...' : '确认删除'}
+            </AlertDialogAction>
+          </AlertDialogFooter>
+        </AlertDialogContent>
+      </AlertDialog>
+
+      {/* 状态切换确认对话框 */}
+      <AlertDialog open={isStatusDialogOpen} onOpenChange={setIsStatusDialogOpen}>
+        <AlertDialogContent>
+          <AlertDialogHeader>
+            <AlertDialogTitle>
+              {selectedCategory?.state === 1 ? '禁用' : '启用'}确认
+            </AlertDialogTitle>
+            <AlertDialogDescription>
+              确定要{selectedCategory?.state === 1 ? '禁用' : '启用'}商品分类 "{selectedCategory?.name}" 吗?
+            </AlertDialogDescription>
+          </AlertDialogHeader>
+          <AlertDialogFooter>
+            <AlertDialogCancel>取消</AlertDialogCancel>
+            <AlertDialogAction
+              onClick={() => handleToggleStatus(selectedCategory?.state === 1 ? 2 : 1)}
+              disabled={toggleStatusMutation.isPending}
+            >
+              {toggleStatusMutation.isPending ? '处理中...' : '确认'}
+            </AlertDialogAction>
+          </AlertDialogFooter>
+        </AlertDialogContent>
+      </AlertDialog>
+    </div>
+  );
+};

+ 4 - 0
packages/goods-category-management-ui-mt/src/components/index.ts

@@ -1,3 +1,7 @@
 // 导出商品分类管理组件
 export { GoodsCategoryManagement } from './GoodsCategoryManagement';
+export { GoodsCategoryTreeManagement } from './GoodsCategoryTreeManagement';
+export { GoodsCategoryTreeAsync } from './GoodsCategoryTreeAsync';
+export { GoodsCategoryForm } from './GoodsCategoryForm';
+export { default as GoodsCategorySelector } from './GoodsCategorySelector';
 export { default as GoodsCategoryCascadeSelector } from './GoodsCategoryCascadeSelector';

+ 19 - 0
packages/goods-category-management-ui-mt/src/types/goodsCategory.ts

@@ -19,4 +19,23 @@ export interface GoodsCategoryFormData {
   image?: string;
   sortOrder: number;
   isActive: boolean;
+}
+
+// 商品分类节点类型定义(用于树形结构)
+export interface GoodsCategoryNode {
+  id: number;
+  name: string;
+  parentId: number | null;
+  level: number;
+  state: number;
+  imageFileId?: number | null;
+  imageFile?: {
+    id: number;
+    fullUrl: string;
+  };
+  createdAt?: string;
+  updatedAt?: string;
+  createdBy?: number | null;
+  updatedBy?: number | null;
+  children?: GoodsCategoryNode[];
 }