|
|
@@ -1,463 +0,0 @@
|
|
|
-import React from 'react';
|
|
|
-import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
|
-import { Button } from '@/client/components/ui/button';
|
|
|
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/client/components/ui/card';
|
|
|
-import { Plus } from 'lucide-react';
|
|
|
-import { useState } from 'react';
|
|
|
-import { areaClient } from '@/client/api';
|
|
|
-import type { InferResponseType, InferRequestType } from 'hono/client';
|
|
|
-import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/client/components/ui/dialog';
|
|
|
-import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/client/components/ui/alert-dialog';
|
|
|
-import { AreaForm } from '../components/AreaForm';
|
|
|
-import { AreaTreeAsync } from '../components/AreaTreeAsync';
|
|
|
-import type { CreateAreaInput, UpdateAreaInput } from '@d8d/geo-areas/schemas';
|
|
|
-import { toast } from 'sonner';
|
|
|
-
|
|
|
-// 类型提取规范
|
|
|
-type AreaResponse = InferResponseType<typeof areaClient.$get, 200>['data'][0];
|
|
|
-type CreateAreaRequest = InferRequestType<typeof areaClient.$post>['json'];
|
|
|
-type UpdateAreaRequest = InferRequestType<typeof areaClient[':id']['$put']>['json'];
|
|
|
-
|
|
|
-// 树形节点类型
|
|
|
-interface AreaNode {
|
|
|
- id: number;
|
|
|
- name: string;
|
|
|
- code: string;
|
|
|
- level: number;
|
|
|
- parentId: number | null;
|
|
|
- isDisabled: number;
|
|
|
- children?: AreaNode[];
|
|
|
-}
|
|
|
-
|
|
|
-// 统一操作处理函数
|
|
|
-const handleOperation = async (operation: () => Promise<any>) => {
|
|
|
- try {
|
|
|
- await operation();
|
|
|
- // toast.success('操作成功');
|
|
|
- console.log('操作成功');
|
|
|
- } catch (error) {
|
|
|
- console.error('操作失败:', error);
|
|
|
- // toast.error('操作失败,请重试');
|
|
|
- throw error;
|
|
|
- }
|
|
|
-};
|
|
|
-
|
|
|
-
|
|
|
-export const AreasTreePage: 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 [selectedArea, setSelectedArea] = useState<AreaResponse | null>(null);
|
|
|
- const [isAddChildDialogOpen, setIsAddChildDialogOpen] = useState(false);
|
|
|
- const [parentAreaForChild, setParentAreaForChild] = useState<AreaNode | null>(null);
|
|
|
-
|
|
|
- // 查询省级数据(异步加载)
|
|
|
- const { data: provinceData, isLoading: isProvinceLoading } = useQuery({
|
|
|
- queryKey: ['areas-tree-province'],
|
|
|
- queryFn: async () => {
|
|
|
- const res = await areaClient.$get({
|
|
|
- query: {
|
|
|
- page: 1,
|
|
|
- pageSize: 100 ,
|
|
|
- filters: JSON.stringify({ level: 1}),
|
|
|
- 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: CreateAreaRequest) => {
|
|
|
- await handleOperation(async () => {
|
|
|
- const res = await areaClient.$post({ json: data });
|
|
|
- if (res.status !== 201) throw new Error('创建省市区失败');
|
|
|
- });
|
|
|
- },
|
|
|
- onSuccess: (_, variables) => {
|
|
|
- // 更新根级缓存
|
|
|
- queryClient.invalidateQueries({ queryKey: ['areas-tree-province'] });
|
|
|
-
|
|
|
- // 如果创建的是子节点,更新父节点的子树缓存
|
|
|
- if (variables.parentId) {
|
|
|
- queryClient.invalidateQueries({ queryKey: ['areas-subtree', variables.parentId] });
|
|
|
- }
|
|
|
-
|
|
|
- // 显示成功提示
|
|
|
- toast.success('省市区创建成功');
|
|
|
-
|
|
|
- // 关闭对话框
|
|
|
- setIsCreateDialogOpen(false);
|
|
|
-
|
|
|
- // 如果是创建子节点,还需要关闭子节点对话框
|
|
|
- if (variables.parentId) {
|
|
|
- setIsAddChildDialogOpen(false);
|
|
|
- setParentAreaForChild(null);
|
|
|
- }
|
|
|
- },
|
|
|
- onError: () => {
|
|
|
- toast.error('创建失败,请重试');
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
- // 更新省市区
|
|
|
- const updateMutation = useMutation({
|
|
|
- mutationFn: async ({ id, data }: { id: number; data: UpdateAreaRequest }) => {
|
|
|
- await handleOperation(async () => {
|
|
|
- const res = await areaClient[':id'].$put({
|
|
|
- param: { id },
|
|
|
- json: data
|
|
|
- });
|
|
|
- if (res.status !== 200) throw new Error('更新省市区失败');
|
|
|
- });
|
|
|
- },
|
|
|
- onSuccess: () => {
|
|
|
- // 更新根级缓存
|
|
|
- queryClient.invalidateQueries({ queryKey: ['areas-tree-province'] });
|
|
|
-
|
|
|
- // 如果更新的节点有父节点,更新父节点的子树缓存
|
|
|
- if (selectedArea?.parentId) {
|
|
|
- queryClient.invalidateQueries({ queryKey: ['areas-subtree', selectedArea.parentId] });
|
|
|
- }
|
|
|
-
|
|
|
- // 显示成功提示
|
|
|
- toast.success('省市区更新成功');
|
|
|
-
|
|
|
- setIsEditDialogOpen(false);
|
|
|
- setSelectedArea(null);
|
|
|
- },
|
|
|
- onError: () => {
|
|
|
- toast.error('更新失败,请重试');
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
- // 删除省市区
|
|
|
- const deleteMutation = useMutation({
|
|
|
- mutationFn: async (id: number) => {
|
|
|
- await handleOperation(async () => {
|
|
|
- const res = await areaClient[':id'].$delete({
|
|
|
- param: { id }
|
|
|
- });
|
|
|
- if (res.status !== 204) throw new Error('删除省市区失败');
|
|
|
- });
|
|
|
- },
|
|
|
- onSuccess: () => {
|
|
|
- // 更新根级缓存
|
|
|
- queryClient.invalidateQueries({ queryKey: ['areas-tree-province'] });
|
|
|
-
|
|
|
- // 如果删除的节点有父节点,更新父节点的子树缓存
|
|
|
- if (selectedArea?.parentId) {
|
|
|
- queryClient.invalidateQueries({ queryKey: ['areas-subtree', selectedArea.parentId] });
|
|
|
- }
|
|
|
-
|
|
|
- // 显示成功提示
|
|
|
- toast.success('省市区删除成功');
|
|
|
-
|
|
|
- setIsDeleteDialogOpen(false);
|
|
|
- setSelectedArea(null);
|
|
|
- },
|
|
|
- onError: () => {
|
|
|
- toast.error('删除失败,请重试');
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
- // 启用/禁用省市区
|
|
|
- const toggleStatusMutation = useMutation({
|
|
|
- mutationFn: async ({ id, isDisabled }: { id: number; isDisabled: number }) => {
|
|
|
- await handleOperation(async () => {
|
|
|
- const res = await areaClient[':id'].$put({
|
|
|
- param: { id },
|
|
|
- json: { isDisabled }
|
|
|
- });
|
|
|
- if (res.status !== 200) throw new Error('更新省市区状态失败');
|
|
|
- });
|
|
|
- },
|
|
|
- onSuccess: () => {
|
|
|
- // 更新根级缓存
|
|
|
- queryClient.invalidateQueries({ queryKey: ['areas-tree-province'] });
|
|
|
-
|
|
|
- // 如果状态切换的节点有父节点,更新父节点的子树缓存
|
|
|
- if (selectedArea?.parentId) {
|
|
|
- queryClient.invalidateQueries({ queryKey: ['areas-subtree', selectedArea.parentId] });
|
|
|
- }
|
|
|
-
|
|
|
- // 显示成功提示
|
|
|
- toast.success(`省市区${selectedArea?.isDisabled === 0 ? '禁用' : '启用'}成功`);
|
|
|
-
|
|
|
- setIsStatusDialogOpen(false);
|
|
|
- setSelectedArea(null);
|
|
|
- },
|
|
|
- onError: () => {
|
|
|
- toast.error('状态切换失败,请重试');
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
-
|
|
|
- // 处理创建省市区
|
|
|
- const handleCreateArea = async (data: CreateAreaInput | UpdateAreaInput) => {
|
|
|
- await createMutation.mutateAsync(data as CreateAreaInput);
|
|
|
- };
|
|
|
-
|
|
|
- // 处理更新省市区
|
|
|
- const handleUpdateArea = async (data: UpdateAreaInput) => {
|
|
|
- if (!selectedArea) return;
|
|
|
- await updateMutation.mutateAsync({ id: selectedArea.id, data });
|
|
|
- };
|
|
|
-
|
|
|
- // 处理删除省市区
|
|
|
- const handleDeleteArea = async () => {
|
|
|
- if (!selectedArea) return;
|
|
|
- await deleteMutation.mutateAsync(selectedArea.id);
|
|
|
- };
|
|
|
-
|
|
|
- // 处理启用/禁用省市区
|
|
|
- const handleToggleStatus = async (isDisabled: number) => {
|
|
|
- if (!selectedArea) return;
|
|
|
- await toggleStatusMutation.mutateAsync({ id: selectedArea.id, isDisabled });
|
|
|
- };
|
|
|
-
|
|
|
- // 处理新增子节点
|
|
|
- const handleAddChild = (area: AreaNode) => {
|
|
|
- setParentAreaForChild(area);
|
|
|
- setIsAddChildDialogOpen(true);
|
|
|
- };
|
|
|
-
|
|
|
- // 处理创建子节点
|
|
|
- const handleCreateChildArea = async (data: CreateAreaInput | UpdateAreaInput) => {
|
|
|
- await createMutation.mutateAsync(data as CreateAreaInput);
|
|
|
- };
|
|
|
-
|
|
|
- // 打开编辑对话框
|
|
|
- const handleEdit = (area: AreaNode) => {
|
|
|
- // 将 AreaNode 转换为 AreaResponse
|
|
|
- const areaResponse: AreaResponse = {
|
|
|
- ...area,
|
|
|
- isDeleted: 0,
|
|
|
- createdAt: new Date().toISOString(),
|
|
|
- updatedAt: new Date().toISOString(),
|
|
|
- createdBy: null,
|
|
|
- updatedBy: null
|
|
|
- };
|
|
|
- setSelectedArea(areaResponse);
|
|
|
- setIsEditDialogOpen(true);
|
|
|
- };
|
|
|
-
|
|
|
- // 打开删除对话框
|
|
|
- const handleDelete = (area: AreaNode) => {
|
|
|
- // 将 AreaNode 转换为 AreaResponse
|
|
|
- const areaResponse: AreaResponse = {
|
|
|
- ...area,
|
|
|
- isDeleted: 0,
|
|
|
- createdAt: new Date().toISOString(),
|
|
|
- updatedAt: new Date().toISOString(),
|
|
|
- createdBy: null,
|
|
|
- updatedBy: null
|
|
|
- };
|
|
|
- setSelectedArea(areaResponse);
|
|
|
- setIsDeleteDialogOpen(true);
|
|
|
- };
|
|
|
-
|
|
|
- // 打开状态切换对话框
|
|
|
- const handleToggleStatusDialog = (area: AreaNode) => {
|
|
|
- // 将 AreaNode 转换为 AreaResponse
|
|
|
- const areaResponse: AreaResponse = {
|
|
|
- ...area,
|
|
|
- isDeleted: 0,
|
|
|
- createdAt: new Date().toISOString(),
|
|
|
- updatedAt: new Date().toISOString(),
|
|
|
- createdBy: null,
|
|
|
- updatedBy: null
|
|
|
- };
|
|
|
- setSelectedArea(areaResponse);
|
|
|
- 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>
|
|
|
- {/* 树形视图 */}
|
|
|
- {isProvinceLoading ? (
|
|
|
- <div className="text-center py-8">
|
|
|
- 加载中...
|
|
|
- </div>
|
|
|
- ) : !provinceData || provinceData.length === 0 ? (
|
|
|
- <div className="text-center py-8">
|
|
|
- 暂无数据
|
|
|
- </div>
|
|
|
- ) : (
|
|
|
- <AreaTreeAsync
|
|
|
- areas={provinceData}
|
|
|
- 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>
|
|
|
- <AreaForm
|
|
|
- onSubmit={handleCreateArea}
|
|
|
- isLoading={createMutation.isPending}
|
|
|
- onCancel={() => setIsCreateDialogOpen(false)}
|
|
|
- smartLevel={1} // 默认设置为省级
|
|
|
- />
|
|
|
- </DialogContent>
|
|
|
- </Dialog>
|
|
|
-
|
|
|
- {/* 编辑省市区对话框 */}
|
|
|
- <Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
|
|
|
- <DialogContent className="max-w-2xl">
|
|
|
- <DialogHeader>
|
|
|
- <DialogTitle>编辑省市区</DialogTitle>
|
|
|
- <DialogDescription>
|
|
|
- 修改省市区信息
|
|
|
- </DialogDescription>
|
|
|
- </DialogHeader>
|
|
|
- {selectedArea && (
|
|
|
- <AreaForm
|
|
|
- area={{
|
|
|
- id: selectedArea.id,
|
|
|
- parentId: selectedArea.parentId || 0,
|
|
|
- name: selectedArea.name,
|
|
|
- level: selectedArea.level,
|
|
|
- code: selectedArea.code,
|
|
|
- isDisabled: selectedArea.isDisabled
|
|
|
- }}
|
|
|
- onSubmit={handleUpdateArea}
|
|
|
- isLoading={updateMutation.isPending}
|
|
|
- onCancel={() => {
|
|
|
- setIsEditDialogOpen(false);
|
|
|
- setSelectedArea(null);
|
|
|
- }}
|
|
|
- />
|
|
|
- )}
|
|
|
- </DialogContent>
|
|
|
- </Dialog>
|
|
|
-
|
|
|
- {/* 新增子节点对话框 */}
|
|
|
- <Dialog open={isAddChildDialogOpen} onOpenChange={setIsAddChildDialogOpen}>
|
|
|
- <DialogContent className="max-w-2xl">
|
|
|
- <DialogHeader>
|
|
|
- <DialogTitle>
|
|
|
- {parentAreaForChild?.level === 1 ? '新增市' : '新增区'}
|
|
|
- </DialogTitle>
|
|
|
- <DialogDescription>
|
|
|
- {parentAreaForChild?.level === 1
|
|
|
- ? `在省份 "${parentAreaForChild?.name}" 下新增市`
|
|
|
- : `在城市 "${parentAreaForChild?.name}" 下新增区/县`}
|
|
|
- </DialogDescription>
|
|
|
- </DialogHeader>
|
|
|
- <AreaForm
|
|
|
- onSubmit={handleCreateChildArea}
|
|
|
- isLoading={createMutation.isPending}
|
|
|
- onCancel={() => {
|
|
|
- setIsAddChildDialogOpen(false);
|
|
|
- setParentAreaForChild(null);
|
|
|
- }}
|
|
|
- smartLevel={(parentAreaForChild?.level ?? 0) + 1}
|
|
|
- smartParentId={parentAreaForChild?.id}
|
|
|
- />
|
|
|
- </DialogContent>
|
|
|
- </Dialog>
|
|
|
-
|
|
|
- {/* 删除确认对话框 */}
|
|
|
- <AlertDialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
|
|
|
- <AlertDialogContent>
|
|
|
- <AlertDialogHeader>
|
|
|
- <AlertDialogTitle>确认删除</AlertDialogTitle>
|
|
|
- <AlertDialogDescription>
|
|
|
- 确定要删除省市区 "{selectedArea?.name}" 吗?此操作不可恢复。
|
|
|
- </AlertDialogDescription>
|
|
|
- </AlertDialogHeader>
|
|
|
- <AlertDialogFooter>
|
|
|
- <AlertDialogCancel>取消</AlertDialogCancel>
|
|
|
- <AlertDialogAction
|
|
|
- onClick={handleDeleteArea}
|
|
|
- disabled={deleteMutation.isPending}
|
|
|
- >
|
|
|
- {deleteMutation.isPending ? '删除中...' : '确认删除'}
|
|
|
- </AlertDialogAction>
|
|
|
- </AlertDialogFooter>
|
|
|
- </AlertDialogContent>
|
|
|
- </AlertDialog>
|
|
|
-
|
|
|
- {/* 状态切换确认对话框 */}
|
|
|
- <AlertDialog open={isStatusDialogOpen} onOpenChange={setIsStatusDialogOpen}>
|
|
|
- <AlertDialogContent>
|
|
|
- <AlertDialogHeader>
|
|
|
- <AlertDialogTitle>
|
|
|
- {selectedArea?.isDisabled === 0 ? '禁用' : '启用'}确认
|
|
|
- </AlertDialogTitle>
|
|
|
- <AlertDialogDescription>
|
|
|
- 确定要{selectedArea?.isDisabled === 0 ? '禁用' : '启用'}省市区 "{selectedArea?.name}" 吗?
|
|
|
- </AlertDialogDescription>
|
|
|
- </AlertDialogHeader>
|
|
|
- <AlertDialogFooter>
|
|
|
- <AlertDialogCancel>取消</AlertDialogCancel>
|
|
|
- <AlertDialogAction
|
|
|
- onClick={() => handleToggleStatus(selectedArea?.isDisabled === 0 ? 1 : 0)}
|
|
|
- disabled={toggleStatusMutation.isPending}
|
|
|
- >
|
|
|
- {toggleStatusMutation.isPending ? '处理中...' : '确认'}
|
|
|
- </AlertDialogAction>
|
|
|
- </AlertDialogFooter>
|
|
|
- </AlertDialogContent>
|
|
|
- </AlertDialog>
|
|
|
- </div>
|
|
|
- );
|
|
|
-};
|