SystemConfigManagement.tsx 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525
  1. import React, { useState } from 'react';
  2. import { useQuery, useMutation } from '@tanstack/react-query';
  3. import { Plus, Edit, Trash2, Search } from 'lucide-react';
  4. import { format } from 'date-fns';
  5. import { Input } from '@d8d/shared-ui-components/components/ui/input';
  6. import { Textarea } from '@d8d/shared-ui-components/components/ui/textarea';
  7. import { Button } from '@d8d/shared-ui-components/components/ui/button';
  8. import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@d8d/shared-ui-components/components/ui/card';
  9. import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@d8d/shared-ui-components/components/ui/table';
  10. import { Skeleton } from '@d8d/shared-ui-components/components/ui/skeleton';
  11. import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@d8d/shared-ui-components/components/ui/dialog';
  12. import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@d8d/shared-ui-components/components/ui/form';
  13. import { useForm } from 'react-hook-form';
  14. import { zodResolver } from '@hookform/resolvers/zod';
  15. import { toast } from 'sonner';
  16. import { DataTablePagination } from '@d8d/shared-ui-components/components/admin/DataTablePagination';
  17. import { systemConfigClientManager } from '../api/systemConfigClient';
  18. import { CreateSystemConfigSchema, UpdateSystemConfigSchema } from '@d8d/core-module-mt/system-config-module-mt/schemas';
  19. import type { SystemConfigFormData, SystemConfigSearchParams, CreateSystemConfigRequest, UpdateSystemConfigRequest, SystemConfigResponse } from '../types';
  20. type CreateRequest = CreateSystemConfigRequest;
  21. type UpdateRequest = UpdateSystemConfigRequest;
  22. const createFormSchema = CreateSystemConfigSchema;
  23. const updateFormSchema = UpdateSystemConfigSchema;
  24. export const SystemConfigManagement: React.FC = () => {
  25. const [searchParams, setSearchParams] = useState<SystemConfigSearchParams>({ page: 1, limit: 10, search: '' });
  26. const [isModalOpen, setIsModalOpen] = useState(false);
  27. const [editingSystemConfig, setEditingSystemConfig] = useState<SystemConfigResponse | null>(null);
  28. const [isCreateForm, setIsCreateForm] = useState(true);
  29. const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
  30. const [systemConfigToDelete, setSystemConfigToDelete] = useState<number | null>(null);
  31. // 表单实例
  32. const createForm = useForm({
  33. resolver: zodResolver(createFormSchema),
  34. defaultValues: {
  35. configKey: '',
  36. configValue: '',
  37. description: ''
  38. }
  39. });
  40. const updateForm = useForm({
  41. resolver: zodResolver(updateFormSchema),
  42. defaultValues: {}
  43. });
  44. // 数据查询
  45. const { data, isLoading, refetch } = useQuery({
  46. queryKey: ['system-configs', searchParams],
  47. queryFn: async () => {
  48. const res = await systemConfigClientManager.get().index.$get({
  49. query: {
  50. page: searchParams.page,
  51. pageSize: searchParams.limit,
  52. keyword: searchParams.search
  53. }
  54. });
  55. if (res.status !== 200) throw new Error('获取系统配置列表失败');
  56. return await res.json();
  57. }
  58. });
  59. // 创建系统配置
  60. const createMutation = useMutation({
  61. mutationFn: async (data: CreateRequest) => {
  62. const res = await systemConfigClientManager.get().index.$post({ json: data });
  63. if (res.status !== 201) throw new Error('创建系统配置失败');
  64. return await res.json();
  65. },
  66. onSuccess: () => {
  67. toast.success('系统配置创建成功');
  68. setIsModalOpen(false);
  69. createForm.reset();
  70. refetch();
  71. },
  72. onError: (error) => {
  73. toast.error(error instanceof Error ? error.message : '创建系统配置失败');
  74. }
  75. });
  76. // 更新系统配置
  77. const updateMutation = useMutation({
  78. mutationFn: async ({ id, data }: { id: number; data: UpdateRequest }) => {
  79. const res = await systemConfigClientManager.get()[':id']['$put']({
  80. param: { id },
  81. json: data
  82. });
  83. if (res.status !== 200) throw new Error('更新系统配置失败');
  84. return await res.json();
  85. },
  86. onSuccess: () => {
  87. toast.success('系统配置更新成功');
  88. setIsModalOpen(false);
  89. setEditingSystemConfig(null);
  90. refetch();
  91. },
  92. onError: (error) => {
  93. toast.error(error instanceof Error ? error.message : '更新系统配置失败');
  94. }
  95. });
  96. // 删除系统配置
  97. const deleteMutation = useMutation({
  98. mutationFn: async (id: number) => {
  99. const res = await systemConfigClientManager.get()[':id']['$delete']({
  100. param: { id }
  101. });
  102. if (res.status !== 204) throw new Error('删除系统配置失败');
  103. return await res.json();
  104. },
  105. onSuccess: () => {
  106. toast.success('系统配置删除成功');
  107. setDeleteDialogOpen(false);
  108. setSystemConfigToDelete(null);
  109. refetch();
  110. },
  111. onError: (error) => {
  112. toast.error(error instanceof Error ? error.message : '删除系统配置失败');
  113. }
  114. });
  115. // 处理搜索
  116. const handleSearch = (e: React.FormEvent) => {
  117. e.preventDefault();
  118. setSearchParams((prev: SystemConfigSearchParams) => ({ ...prev, page: 1 }));
  119. refetch();
  120. };
  121. // 处理创建系统配置
  122. const handleCreateSystemConfig = () => {
  123. setIsCreateForm(true);
  124. setEditingSystemConfig(null);
  125. createForm.reset();
  126. setIsModalOpen(true);
  127. };
  128. // 处理编辑系统配置
  129. const handleEditSystemConfig = (systemConfig: SystemConfigResponse) => {
  130. setIsCreateForm(false);
  131. setEditingSystemConfig(systemConfig);
  132. updateForm.reset({
  133. configKey: systemConfig.configKey || undefined,
  134. configValue: systemConfig.configValue || undefined,
  135. description: systemConfig.description || undefined
  136. });
  137. setIsModalOpen(true);
  138. };
  139. // 处理删除系统配置
  140. const handleDeleteSystemConfig = (id: number) => {
  141. setSystemConfigToDelete(id);
  142. setDeleteDialogOpen(true);
  143. };
  144. // 确认删除
  145. const confirmDelete = () => {
  146. if (systemConfigToDelete) {
  147. deleteMutation.mutate(systemConfigToDelete);
  148. }
  149. };
  150. // 处理创建表单提交
  151. const handleCreateSubmit = async (data: SystemConfigFormData) => {
  152. try {
  153. await createMutation.mutateAsync(data);
  154. } catch (error) {
  155. throw error;
  156. }
  157. };
  158. // 处理编辑表单提交
  159. const handleUpdateSubmit = async (data: any) => {
  160. if (!editingSystemConfig) return;
  161. try {
  162. await updateMutation.mutateAsync({
  163. id: editingSystemConfig.id,
  164. data
  165. });
  166. } catch (error) {
  167. throw error;
  168. }
  169. };
  170. return (
  171. <div className="space-y-4">
  172. <div className="flex justify-between items-center">
  173. <h1 className="text-2xl font-bold">系统配置管理</h1>
  174. <Button onClick={handleCreateSystemConfig}>
  175. <Plus className="mr-2 h-4 w-4" />
  176. 创建配置
  177. </Button>
  178. </div>
  179. <Card>
  180. <CardHeader>
  181. <CardTitle>系统配置列表</CardTitle>
  182. <CardDescription>管理系统所有配置项,包括小程序配置、支付参数等</CardDescription>
  183. </CardHeader>
  184. <CardContent>
  185. <div className="mb-4">
  186. <form onSubmit={handleSearch} className="flex gap-2">
  187. <div className="relative flex-1 max-w-sm">
  188. <Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
  189. <Input
  190. placeholder="搜索配置键或描述..."
  191. value={searchParams.search}
  192. onChange={(e) => setSearchParams((prev: SystemConfigSearchParams) => ({ ...prev, search: e.target.value }))}
  193. className="pl-8"
  194. data-testid="search-input"
  195. />
  196. </div>
  197. <Button type="submit" variant="outline">
  198. 搜索
  199. </Button>
  200. </form>
  201. </div>
  202. <div className="rounded-md border">
  203. <div className="relative w-full overflow-x-auto">
  204. <Table>
  205. <TableHeader>
  206. <TableRow>
  207. <TableHead>ID</TableHead>
  208. <TableHead>配置键</TableHead>
  209. <TableHead>配置值</TableHead>
  210. <TableHead>描述</TableHead>
  211. <TableHead>创建时间</TableHead>
  212. <TableHead>更新时间</TableHead>
  213. <TableHead className="text-right">操作</TableHead>
  214. </TableRow>
  215. </TableHeader>
  216. <TableBody>
  217. {isLoading ? (
  218. Array.from({ length: 5 }).map((_, index) => (
  219. <TableRow key={index}>
  220. <TableCell>
  221. <Skeleton className="h-4 w-8" />
  222. </TableCell>
  223. <TableCell>
  224. <Skeleton className="h-4 w-12" />
  225. </TableCell>
  226. <TableCell>
  227. <Skeleton className="h-4 w-32" />
  228. </TableCell>
  229. <TableCell>
  230. <Skeleton className="h-4 w-40" />
  231. </TableCell>
  232. <TableCell>
  233. <Skeleton className="h-4 w-24" />
  234. </TableCell>
  235. <TableCell>
  236. <Skeleton className="h-4 w-24" />
  237. </TableCell>
  238. <TableCell>
  239. <Skeleton className="h-4 w-24" />
  240. </TableCell>
  241. <TableCell>
  242. <div className="flex justify-end gap-2">
  243. <Skeleton className="h-8 w-8 rounded" />
  244. <Skeleton className="h-8 w-8 rounded" />
  245. </div>
  246. </TableCell>
  247. </TableRow>
  248. ))
  249. ) : data?.data && data.data.length > 0 ? (
  250. data.data.map((systemConfig) => (
  251. <TableRow key={systemConfig.id}>
  252. <TableCell>{systemConfig.id}</TableCell>
  253. <TableCell>
  254. <code className="text-xs bg-muted px-1 rounded">{systemConfig.configKey}</code>
  255. </TableCell>
  256. <TableCell>
  257. <div className="max-w-xs truncate">
  258. {systemConfig.configValue}
  259. </div>
  260. </TableCell>
  261. <TableCell>
  262. {systemConfig.description || '-'}
  263. </TableCell>
  264. <TableCell>
  265. {systemConfig.createdAt ? format(new Date(systemConfig.createdAt), 'yyyy-MM-dd HH:mm') : '-'}
  266. </TableCell>
  267. <TableCell>
  268. {systemConfig.updatedAt ? format(new Date(systemConfig.updatedAt), 'yyyy-MM-dd HH:mm') : '-'}
  269. </TableCell>
  270. <TableCell className="text-right">
  271. <div className="flex justify-end gap-2">
  272. <Button
  273. variant="ghost"
  274. size="icon"
  275. onClick={() => handleEditSystemConfig(systemConfig)}
  276. data-testid={`edit-button-${systemConfig.id}`}
  277. >
  278. <Edit className="h-4 w-4" />
  279. </Button>
  280. <Button
  281. variant="ghost"
  282. size="icon"
  283. onClick={() => handleDeleteSystemConfig(systemConfig.id)}
  284. data-testid={`delete-button-${systemConfig.id}`}
  285. >
  286. <Trash2 className="h-4 w-4" />
  287. </Button>
  288. </div>
  289. </TableCell>
  290. </TableRow>
  291. ))
  292. ) : (
  293. <TableRow>
  294. <TableCell colSpan={8} className="text-center py-8">
  295. <p className="text-muted-foreground">暂无系统配置数据</p>
  296. </TableCell>
  297. </TableRow>
  298. )}
  299. </TableBody>
  300. </Table>
  301. </div>
  302. </div>
  303. <DataTablePagination
  304. currentPage={searchParams.page}
  305. pageSize={searchParams.limit}
  306. totalCount={data?.pagination.total || 0}
  307. onPageChange={(page, limit) => setSearchParams((prev: SystemConfigSearchParams) => ({ ...prev, page, limit }))}
  308. />
  309. </CardContent>
  310. </Card>
  311. {/* 创建/编辑对话框 */}
  312. <Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
  313. <DialogContent className="sm:max-w-[600px] max-h-[90vh] overflow-y-auto">
  314. <DialogHeader>
  315. <DialogTitle>{isCreateForm ? '创建系统配置' : '编辑系统配置'}</DialogTitle>
  316. <DialogDescription>
  317. {isCreateForm ? '创建一个新的系统配置项' : '编辑现有系统配置信息'}
  318. </DialogDescription>
  319. </DialogHeader>
  320. {isCreateForm ? (
  321. // 创建表单(独立渲染)
  322. <Form {...createForm}>
  323. <form onSubmit={createForm.handleSubmit(handleCreateSubmit)} className="space-y-4">
  324. <FormField
  325. control={createForm.control}
  326. name="configKey"
  327. render={({ field }) => (
  328. <FormItem>
  329. <FormLabel className="flex items-center">
  330. 配置键 <span className="text-red-500 ml-1">*</span>
  331. </FormLabel>
  332. <FormControl>
  333. <Input
  334. placeholder="请输入配置键,如:app.login.enabled"
  335. {...field}
  336. data-testid="config-key-input"
  337. />
  338. </FormControl>
  339. <FormDescription>配置项的唯一标识,使用点分隔符命名,如:app.login.enabled</FormDescription>
  340. <FormMessage />
  341. </FormItem>
  342. )}
  343. />
  344. <FormField
  345. control={createForm.control}
  346. name="configValue"
  347. render={({ field }) => (
  348. <FormItem>
  349. <FormLabel className="flex items-center">
  350. 配置值 <span className="text-red-500 ml-1">*</span>
  351. </FormLabel>
  352. <FormControl>
  353. <Textarea
  354. placeholder="请输入配置值"
  355. {...field}
  356. data-testid="config-value-input"
  357. />
  358. </FormControl>
  359. <FormDescription>配置项的具体值,可以是字符串、数字或布尔值</FormDescription>
  360. <FormMessage />
  361. </FormItem>
  362. )}
  363. />
  364. <FormField
  365. control={createForm.control}
  366. name="description"
  367. render={({ field }) => (
  368. <FormItem>
  369. <FormLabel>配置描述</FormLabel>
  370. <FormControl>
  371. <Textarea
  372. placeholder="请输入配置描述"
  373. {...field}
  374. data-testid="description-input"
  375. />
  376. </FormControl>
  377. <FormDescription>配置项的详细说明和用途描述</FormDescription>
  378. <FormMessage />
  379. </FormItem>
  380. )}
  381. />
  382. <DialogFooter>
  383. <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
  384. 取消
  385. </Button>
  386. <Button type="submit" disabled={createMutation.isPending} data-testid="create-submit-button">
  387. 创建
  388. </Button>
  389. </DialogFooter>
  390. </form>
  391. </Form>
  392. ) : (
  393. // 编辑表单(独立渲染)
  394. <Form {...updateForm}>
  395. <form onSubmit={updateForm.handleSubmit(handleUpdateSubmit)} className="space-y-4">
  396. <FormField
  397. control={updateForm.control}
  398. name="configKey"
  399. render={({ field }) => (
  400. <FormItem>
  401. <FormLabel className="flex items-center">
  402. 配置键 <span className="text-red-500 ml-1">*</span>
  403. </FormLabel>
  404. <FormControl>
  405. <Input
  406. placeholder="请输入配置键"
  407. {...field}
  408. data-testid="config-key-input"
  409. />
  410. </FormControl>
  411. <FormDescription>配置项的唯一标识,使用点分隔符命名</FormDescription>
  412. <FormMessage />
  413. </FormItem>
  414. )}
  415. />
  416. <FormField
  417. control={updateForm.control}
  418. name="configValue"
  419. render={({ field }) => (
  420. <FormItem>
  421. <FormLabel className="flex items-center">
  422. 配置值 <span className="text-red-500 ml-1">*</span>
  423. </FormLabel>
  424. <FormControl>
  425. <Textarea
  426. placeholder="请输入配置值"
  427. {...field}
  428. data-testid="config-value-input"
  429. />
  430. </FormControl>
  431. <FormDescription>配置项的具体值,可以是字符串、数字或布尔值</FormDescription>
  432. <FormMessage />
  433. </FormItem>
  434. )}
  435. />
  436. <FormField
  437. control={updateForm.control}
  438. name="description"
  439. render={({ field }) => (
  440. <FormItem>
  441. <FormLabel>配置描述</FormLabel>
  442. <FormControl>
  443. <Textarea
  444. placeholder="请输入配置描述"
  445. {...field}
  446. data-testid="description-input"
  447. />
  448. </FormControl>
  449. <FormDescription>配置项的详细说明和用途描述</FormDescription>
  450. <FormMessage />
  451. </FormItem>
  452. )}
  453. />
  454. <DialogFooter>
  455. <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
  456. 取消
  457. </Button>
  458. <Button type="submit" disabled={updateMutation.isPending} data-testid="update-submit-button">
  459. 更新
  460. </Button>
  461. </DialogFooter>
  462. </form>
  463. </Form>
  464. )}
  465. </DialogContent>
  466. </Dialog>
  467. {/* 删除确认对话框 */}
  468. <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
  469. <DialogContent>
  470. <DialogHeader>
  471. <DialogTitle>确认删除</DialogTitle>
  472. <DialogDescription>
  473. 确定要删除这个系统配置吗?此操作无法撤销。
  474. </DialogDescription>
  475. </DialogHeader>
  476. <DialogFooter>
  477. <Button variant="outline" onClick={() => setDeleteDialogOpen(false)}>
  478. 取消
  479. </Button>
  480. <Button
  481. variant="destructive"
  482. onClick={confirmDelete}
  483. disabled={deleteMutation.isPending}
  484. data-testid="confirm-delete-button"
  485. >
  486. {deleteMutation.isPending ? '删除中...' : '删除'}
  487. </Button>
  488. </DialogFooter>
  489. </DialogContent>
  490. </Dialog>
  491. </div>
  492. );
  493. };
  494. export default SystemConfigManagement;