AreaManagement.tsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477
  1. import React from 'react';
  2. import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
  3. import { Button } from '@d8d/shared-ui-components/components/ui/button';
  4. import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@d8d/shared-ui-components/components/ui/card';
  5. import { Plus } from 'lucide-react';
  6. import { useState } from 'react';
  7. import { areaClient, areaClientManager } from '../api/areaClient';
  8. import type { InferResponseType, InferRequestType } from 'hono/client';
  9. import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@d8d/shared-ui-components/components/ui/dialog';
  10. import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@d8d/shared-ui-components/components/ui/alert-dialog';
  11. import { AreaForm } from './AreaForm';
  12. import { AreaTreeAsync } from './AreaTreeAsync';
  13. import type { CreateAreaInputMt, UpdateAreaInputMt } from '@d8d/geo-areas-mt/schemas';
  14. import { toast } from 'sonner';
  15. // 类型提取规范
  16. type AreaResponse = InferResponseType<typeof areaClient.index.$get, 200>['data'][0];
  17. type CreateAreaRequest = InferRequestType<typeof areaClient.index.$post>['json'];
  18. type UpdateAreaRequest = InferRequestType<typeof areaClient[':id']['$put']>['json'];
  19. // 树形节点类型
  20. interface AreaNode {
  21. id: number;
  22. tenantId: number;
  23. name: string;
  24. code: string;
  25. level: number;
  26. parentId: number | null;
  27. isDisabled: number;
  28. children?: AreaNode[];
  29. }
  30. // 统一操作处理函数
  31. const handleOperation = async (operation: () => Promise<void>) => {
  32. try {
  33. await operation();
  34. // toast.success('操作成功');
  35. } catch (error) {
  36. // toast.error('操作失败,请重试');
  37. throw error;
  38. }
  39. };
  40. export const AreaManagement: React.FC = () => {
  41. const queryClient = useQueryClient();
  42. const [expandedNodes, setExpandedNodes] = useState<Set<number>>(new Set());
  43. const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
  44. const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
  45. const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
  46. const [isStatusDialogOpen, setIsStatusDialogOpen] = useState(false);
  47. const [selectedArea, setSelectedArea] = useState<AreaResponse | null>(null);
  48. const [isAddChildDialogOpen, setIsAddChildDialogOpen] = useState(false);
  49. const [parentAreaForChild, setParentAreaForChild] = useState<AreaNode | null>(null);
  50. // 查询省级数据(异步加载)
  51. const { data: provinceData, isLoading: isProvinceLoading } = useQuery({
  52. queryKey: ['areas-tree-province'],
  53. queryFn: async () => {
  54. const res = await areaClientManager.get().index.$get({
  55. query: {
  56. page: 1,
  57. pageSize: 100 ,
  58. filters: JSON.stringify({ level: 1}),
  59. sortBy: 'id',
  60. sortOrder: 'ASC'
  61. }
  62. });
  63. if (res.status !== 200) throw new Error('获取省级数据失败');
  64. const response = await res.json();
  65. return response.data;
  66. },
  67. staleTime: 5 * 60 * 1000,
  68. gcTime: 10 * 60 * 1000,
  69. });
  70. // 创建省市区
  71. const createMutation = useMutation({
  72. mutationFn: async (data: CreateAreaRequest) => {
  73. await handleOperation(async () => {
  74. const res = await areaClientManager.get().index.$post({ json: data });
  75. if (res.status !== 201) throw new Error('创建省市区失败');
  76. });
  77. },
  78. onSuccess: (_, variables) => {
  79. // 更新根级缓存
  80. queryClient.invalidateQueries({ queryKey: ['areas-tree-province'] });
  81. // 如果创建的是子节点,更新父节点的子树缓存
  82. if (variables.parentId) {
  83. queryClient.invalidateQueries({ queryKey: ['areas-subtree', variables.parentId] });
  84. }
  85. // 显示成功提示
  86. toast.success('省市区创建成功');
  87. // 关闭对话框
  88. setIsCreateDialogOpen(false);
  89. // 如果是创建子节点,还需要关闭子节点对话框
  90. if (variables.parentId) {
  91. setIsAddChildDialogOpen(false);
  92. setParentAreaForChild(null);
  93. }
  94. },
  95. onError: () => {
  96. toast.error('创建失败,请重试');
  97. }
  98. });
  99. // 更新省市区
  100. const updateMutation = useMutation({
  101. mutationFn: async ({ id, data }: { id: number; data: UpdateAreaRequest }) => {
  102. await handleOperation(async () => {
  103. const res = await areaClientManager.get()[':id'].$put({
  104. param: { id },
  105. json: data
  106. });
  107. if (res.status !== 200) throw new Error('更新省市区失败');
  108. });
  109. },
  110. onSuccess: () => {
  111. // 更新根级缓存
  112. queryClient.invalidateQueries({ queryKey: ['areas-tree-province'] });
  113. // 如果更新的节点有父节点,更新父节点的子树缓存
  114. if (selectedArea?.parentId) {
  115. queryClient.invalidateQueries({ queryKey: ['areas-subtree', selectedArea.parentId] });
  116. }
  117. // 显示成功提示
  118. toast.success('省市区更新成功');
  119. setIsEditDialogOpen(false);
  120. setSelectedArea(null);
  121. },
  122. onError: () => {
  123. toast.error('更新失败,请重试');
  124. }
  125. });
  126. // 删除省市区
  127. const deleteMutation = useMutation({
  128. mutationFn: async (id: number) => {
  129. await handleOperation(async () => {
  130. const res = await areaClientManager.get()[':id'].$delete({
  131. param: { id }
  132. });
  133. if (res.status !== 204) throw new Error('删除省市区失败');
  134. });
  135. },
  136. onSuccess: () => {
  137. // 更新根级缓存
  138. queryClient.invalidateQueries({ queryKey: ['areas-tree-province'] });
  139. // 如果删除的节点有父节点,更新父节点的子树缓存
  140. if (selectedArea?.parentId) {
  141. queryClient.invalidateQueries({ queryKey: ['areas-subtree', selectedArea.parentId] });
  142. }
  143. // 显示成功提示
  144. toast.success('省市区删除成功');
  145. setIsDeleteDialogOpen(false);
  146. setSelectedArea(null);
  147. },
  148. onError: () => {
  149. toast.error('删除失败,请重试');
  150. }
  151. });
  152. // 启用/禁用省市区
  153. const toggleStatusMutation = useMutation({
  154. mutationFn: async ({ id, isDisabled }: { id: number; isDisabled: number }) => {
  155. await handleOperation(async () => {
  156. const res = await areaClientManager.get()[':id'].$put({
  157. param: { id },
  158. json: { isDisabled }
  159. });
  160. if (res.status !== 200) throw new Error('更新省市区状态失败');
  161. });
  162. },
  163. onSuccess: () => {
  164. // 更新根级缓存
  165. queryClient.invalidateQueries({ queryKey: ['areas-tree-province'] });
  166. // 如果状态切换的节点有父节点,更新父节点的子树缓存
  167. if (selectedArea?.parentId) {
  168. queryClient.invalidateQueries({ queryKey: ['areas-subtree', selectedArea.parentId] });
  169. }
  170. // 显示成功提示
  171. toast.success(`省市区${selectedArea?.isDisabled === 0 ? '禁用' : '启用'}成功`);
  172. setIsStatusDialogOpen(false);
  173. setSelectedArea(null);
  174. },
  175. onError: () => {
  176. toast.error('状态切换失败,请重试');
  177. }
  178. });
  179. // 处理创建省市区
  180. const handleCreateArea = async (data: CreateAreaInputMt | UpdateAreaInputMt) => {
  181. const createData = {
  182. ...data,
  183. tenantId: 1 // 测试环境使用默认tenantId
  184. } as CreateAreaInputMt;
  185. await createMutation.mutateAsync(createData);
  186. };
  187. // 处理更新省市区
  188. const handleUpdateArea = async (data: UpdateAreaInputMt) => {
  189. if (!selectedArea) return;
  190. const updateData = {
  191. ...data,
  192. tenantId: 1 // 测试环境使用默认tenantId
  193. } as UpdateAreaInputMt;
  194. await updateMutation.mutateAsync({ id: selectedArea.id, data: updateData });
  195. };
  196. // 处理删除省市区
  197. const handleDeleteArea = async () => {
  198. if (!selectedArea) return;
  199. await deleteMutation.mutateAsync(selectedArea.id);
  200. };
  201. // 处理启用/禁用省市区
  202. const handleToggleStatus = async (isDisabled: number) => {
  203. if (!selectedArea) return;
  204. await toggleStatusMutation.mutateAsync({ id: selectedArea.id, isDisabled });
  205. };
  206. // 处理新增子节点
  207. const handleAddChild = (area: AreaNode) => {
  208. setParentAreaForChild(area);
  209. setIsAddChildDialogOpen(true);
  210. };
  211. // 处理创建子节点
  212. const handleCreateChildArea = async (data: CreateAreaInputMt | UpdateAreaInputMt) => {
  213. const createData = {
  214. ...data,
  215. tenantId: 1 // 测试环境使用默认tenantId
  216. } as CreateAreaInputMt;
  217. await createMutation.mutateAsync(createData);
  218. };
  219. // 打开编辑对话框
  220. const handleEdit = (area: AreaNode) => {
  221. // 将 AreaNode 转换为 AreaResponse
  222. const areaResponse: AreaResponse = {
  223. ...area,
  224. isDeleted: 0,
  225. createdAt: new Date().toISOString(),
  226. updatedAt: new Date().toISOString(),
  227. createdBy: null,
  228. updatedBy: null
  229. };
  230. setSelectedArea(areaResponse);
  231. setIsEditDialogOpen(true);
  232. };
  233. // 打开删除对话框
  234. const handleDelete = (area: AreaNode) => {
  235. // 将 AreaNode 转换为 AreaResponse
  236. const areaResponse: AreaResponse = {
  237. ...area,
  238. isDeleted: 0,
  239. createdAt: new Date().toISOString(),
  240. updatedAt: new Date().toISOString(),
  241. createdBy: null,
  242. updatedBy: null
  243. };
  244. setSelectedArea(areaResponse);
  245. setIsDeleteDialogOpen(true);
  246. };
  247. // 打开状态切换对话框
  248. const handleToggleStatusDialog = (area: AreaNode) => {
  249. // 将 AreaNode 转换为 AreaResponse
  250. const areaResponse: AreaResponse = {
  251. ...area,
  252. isDeleted: 0,
  253. createdAt: new Date().toISOString(),
  254. updatedAt: new Date().toISOString(),
  255. createdBy: null,
  256. updatedBy: null
  257. };
  258. setSelectedArea(areaResponse);
  259. setIsStatusDialogOpen(true);
  260. };
  261. // 切换节点展开状态
  262. const handleToggleNode = (nodeId: number) => {
  263. setExpandedNodes(prev => {
  264. const newSet = new Set(prev);
  265. if (newSet.has(nodeId)) {
  266. newSet.delete(nodeId);
  267. } else {
  268. newSet.add(nodeId);
  269. }
  270. return newSet;
  271. });
  272. };
  273. return (
  274. <div className="space-y-6">
  275. <div className="flex items-center justify-between">
  276. <div>
  277. <h1 className="text-3xl font-bold tracking-tight">省市区树形管理</h1>
  278. <p className="text-muted-foreground">
  279. 异步加载树形结构,高效管理省市区数据
  280. </p>
  281. </div>
  282. <Button onClick={() => setIsCreateDialogOpen(true)}>
  283. <Plus className="mr-2 h-4 w-4" />
  284. 新增省
  285. </Button>
  286. </div>
  287. <Card>
  288. <CardHeader>
  289. <CardTitle>省市区树形结构</CardTitle>
  290. <CardDescription>
  291. 以树形结构查看和管理省市区层级关系,默认只加载省级数据
  292. </CardDescription>
  293. </CardHeader>
  294. <CardContent>
  295. {/* 树形视图 */}
  296. {isProvinceLoading ? (
  297. <div className="text-center py-8">
  298. 加载中...
  299. </div>
  300. ) : !provinceData || provinceData.length === 0 ? (
  301. <div className="text-center py-8">
  302. 暂无数据
  303. </div>
  304. ) : (
  305. <AreaTreeAsync
  306. areas={provinceData}
  307. expandedNodes={expandedNodes}
  308. onToggleNode={handleToggleNode}
  309. onEdit={handleEdit}
  310. onDelete={handleDelete}
  311. onToggleStatus={handleToggleStatusDialog}
  312. onAddChild={handleAddChild}
  313. />
  314. )}
  315. </CardContent>
  316. </Card>
  317. {/* 创建省市区对话框 */}
  318. <Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
  319. <DialogContent className="max-w-2xl">
  320. <DialogHeader>
  321. <DialogTitle>新增省</DialogTitle>
  322. <DialogDescription>
  323. 填写省信息
  324. </DialogDescription>
  325. </DialogHeader>
  326. <AreaForm
  327. onSubmit={handleCreateArea}
  328. isLoading={createMutation.isPending}
  329. onCancel={() => setIsCreateDialogOpen(false)}
  330. smartLevel={1} // 默认设置为省级
  331. />
  332. </DialogContent>
  333. </Dialog>
  334. {/* 编辑省市区对话框 */}
  335. <Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
  336. <DialogContent className="max-w-2xl">
  337. <DialogHeader>
  338. <DialogTitle>编辑省市区</DialogTitle>
  339. <DialogDescription>
  340. 修改省市区信息
  341. </DialogDescription>
  342. </DialogHeader>
  343. {selectedArea && (
  344. <AreaForm
  345. area={{
  346. id: selectedArea.id,
  347. parentId: selectedArea.parentId || 0,
  348. name: selectedArea.name,
  349. level: selectedArea.level,
  350. code: selectedArea.code,
  351. isDisabled: selectedArea.isDisabled
  352. }}
  353. onSubmit={handleUpdateArea}
  354. isLoading={updateMutation.isPending}
  355. onCancel={() => {
  356. setIsEditDialogOpen(false);
  357. setSelectedArea(null);
  358. }}
  359. />
  360. )}
  361. </DialogContent>
  362. </Dialog>
  363. {/* 新增子节点对话框 */}
  364. <Dialog open={isAddChildDialogOpen} onOpenChange={setIsAddChildDialogOpen}>
  365. <DialogContent className="max-w-2xl">
  366. <DialogHeader>
  367. <DialogTitle>
  368. {parentAreaForChild?.level === 1 ? '新增市' :
  369. parentAreaForChild?.level === 2 ? '新增区' : '新增乡镇'}
  370. </DialogTitle>
  371. <DialogDescription>
  372. {parentAreaForChild?.level === 1
  373. ? `在省份 "${parentAreaForChild?.name}" 下新增市`
  374. : parentAreaForChild?.level === 2
  375. ? `在城市 "${parentAreaForChild?.name}" 下新增区/县`
  376. : `在区县 "${parentAreaForChild?.name}" 下新增街道/乡镇`}
  377. </DialogDescription>
  378. </DialogHeader>
  379. <AreaForm
  380. onSubmit={handleCreateChildArea}
  381. isLoading={createMutation.isPending}
  382. onCancel={() => {
  383. setIsAddChildDialogOpen(false);
  384. setParentAreaForChild(null);
  385. }}
  386. smartLevel={(parentAreaForChild?.level ?? 0) + 1}
  387. smartParentId={parentAreaForChild?.id}
  388. />
  389. </DialogContent>
  390. </Dialog>
  391. {/* 删除确认对话框 */}
  392. <AlertDialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
  393. <AlertDialogContent>
  394. <AlertDialogHeader>
  395. <AlertDialogTitle>确认删除</AlertDialogTitle>
  396. <AlertDialogDescription>
  397. 确定要删除省市区 "{selectedArea?.name}" 吗?此操作不可恢复。
  398. </AlertDialogDescription>
  399. </AlertDialogHeader>
  400. <AlertDialogFooter>
  401. <AlertDialogCancel>取消</AlertDialogCancel>
  402. <AlertDialogAction
  403. onClick={handleDeleteArea}
  404. disabled={deleteMutation.isPending}
  405. >
  406. {deleteMutation.isPending ? '删除中...' : '确认删除'}
  407. </AlertDialogAction>
  408. </AlertDialogFooter>
  409. </AlertDialogContent>
  410. </AlertDialog>
  411. {/* 状态切换确认对话框 */}
  412. <AlertDialog open={isStatusDialogOpen} onOpenChange={setIsStatusDialogOpen}>
  413. <AlertDialogContent>
  414. <AlertDialogHeader>
  415. <AlertDialogTitle>
  416. {selectedArea?.isDisabled === 0 ? '禁用' : '启用'}确认
  417. </AlertDialogTitle>
  418. <AlertDialogDescription>
  419. 确定要{selectedArea?.isDisabled === 0 ? '禁用' : '启用'}省市区 "{selectedArea?.name}" 吗?
  420. </AlertDialogDescription>
  421. </AlertDialogHeader>
  422. <AlertDialogFooter>
  423. <AlertDialogCancel>取消</AlertDialogCancel>
  424. <AlertDialogAction
  425. onClick={() => handleToggleStatus(selectedArea?.isDisabled === 0 ? 1 : 0)}
  426. disabled={toggleStatusMutation.isPending}
  427. >
  428. {toggleStatusMutation.isPending ? '处理中...' : '确认'}
  429. </AlertDialogAction>
  430. </AlertDialogFooter>
  431. </AlertDialogContent>
  432. </AlertDialog>
  433. </div>
  434. );
  435. };