|
|
@@ -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>
|
|
|
+ );
|
|
|
+};
|