TenantsPage.tsx 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723
  1. import React, { useState, useMemo, useCallback } from 'react';
  2. import { useQuery } from '@tanstack/react-query';
  3. import { format } from 'date-fns';
  4. import { Plus, Search, Edit, Trash2, Filter, X } from 'lucide-react';
  5. import { tenantClient } from '@/api/tenantClient';
  6. import type { InferRequestType, InferResponseType } from 'hono/client';
  7. import { Button, Input, Card, CardContent, CardDescription, CardHeader, CardTitle, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Badge, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, Skeleton, Switch, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Popover, PopoverContent, PopoverTrigger, Calendar } from '@d8d/shared-ui-components';
  8. import { DataTablePagination } from '@/components/DataTablePagination';
  9. import { useForm } from 'react-hook-form';
  10. import { zodResolver } from '@hookform/resolvers/zod';
  11. import { toast } from 'sonner';
  12. import { CreateTenantDto, UpdateTenantDto } from '@d8d/tenant-module-mt/schemas';
  13. import { cn } from '@/utils/cn';
  14. import { formatTenantStatus } from '@/utils/formatTenantStatus';
  15. // 使用RPC方式提取类型
  16. type CreateTenantRequest = InferRequestType<typeof tenantClient.index.$post>['json'];
  17. type UpdateTenantRequest = InferRequestType<typeof tenantClient[':id']['$put']>['json'];
  18. type TenantResponse = InferResponseType<typeof tenantClient.index.$get, 200>['data'][0];
  19. // 直接使用后端定义的 schema
  20. const createTenantFormSchema = CreateTenantDto;
  21. const updateTenantFormSchema = UpdateTenantDto;
  22. type CreateTenantFormData = CreateTenantRequest;
  23. type UpdateTenantFormData = UpdateTenantRequest;
  24. export const TenantsPage = () => {
  25. const [searchParams, setSearchParams] = useState({
  26. page: 1,
  27. limit: 10,
  28. keyword: ''
  29. });
  30. const [filters, setFilters] = useState({
  31. status: undefined as number | undefined,
  32. createdAt: undefined as { gte?: string; lte?: string } | undefined
  33. });
  34. const [showFilters, setShowFilters] = useState(false);
  35. const [isModalOpen, setIsModalOpen] = useState(false);
  36. const [editingTenant, setEditingTenant] = useState<TenantResponse | null>(null);
  37. const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
  38. const [tenantToDelete, setTenantToDelete] = useState<number | null>(null);
  39. const [isCreateForm, setIsCreateForm] = useState(true);
  40. const createForm = useForm<CreateTenantFormData>({
  41. resolver: zodResolver(createTenantFormSchema),
  42. defaultValues: {
  43. name: '',
  44. code: '',
  45. phone: null,
  46. contactName: null,
  47. status: 1,
  48. config: null,
  49. rsaPublicKey: null,
  50. aesKey: null,
  51. },
  52. });
  53. const updateForm = useForm<UpdateTenantFormData>({
  54. resolver: zodResolver(updateTenantFormSchema),
  55. defaultValues: {
  56. name: undefined,
  57. code: undefined,
  58. phone: null,
  59. contactName: null,
  60. status: undefined,
  61. config: null,
  62. rsaPublicKey: null,
  63. aesKey: null,
  64. },
  65. });
  66. const { data: tenantsData, isLoading, refetch } = useQuery({
  67. queryKey: ['tenants', searchParams, filters],
  68. queryFn: async () => {
  69. const filterParams: Record<string, unknown> = {};
  70. if (filters.status !== undefined) {
  71. filterParams.status = filters.status;
  72. }
  73. if (filters.createdAt) {
  74. filterParams.createdAt = filters.createdAt;
  75. }
  76. const res = await tenantClient.index.$get({
  77. query: {
  78. page: searchParams.page,
  79. pageSize: searchParams.limit,
  80. keyword: searchParams.keyword,
  81. filters: Object.keys(filterParams).length > 0 ? JSON.stringify(filterParams) : undefined
  82. }
  83. });
  84. if (res.status !== 200) {
  85. throw new Error('获取租户列表失败');
  86. }
  87. return await res.json();
  88. }
  89. });
  90. const tenants = tenantsData?.data || [];
  91. const totalCount = tenantsData?.pagination?.total || 0;
  92. // 防抖搜索函数
  93. const debounce = (func: Function, delay: number) => {
  94. let timeoutId: NodeJS.Timeout;
  95. return (...args: any[]) => {
  96. clearTimeout(timeoutId);
  97. timeoutId = setTimeout(() => func(...args), delay);
  98. };
  99. };
  100. // 使用useCallback包装防抖搜索
  101. const debouncedSearch = useCallback(
  102. debounce((keyword: string) => {
  103. setSearchParams(prev => ({ ...prev, keyword, page: 1 }));
  104. }, 300),
  105. []
  106. );
  107. // 处理搜索输入变化
  108. const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  109. const keyword = e.target.value;
  110. setSearchParams(prev => ({ ...prev, keyword }));
  111. debouncedSearch(keyword);
  112. };
  113. // 处理搜索表单提交
  114. const handleSearch = (e: React.FormEvent) => {
  115. e.preventDefault();
  116. setSearchParams(prev => ({ ...prev, page: 1 }));
  117. };
  118. // 处理分页
  119. const handlePageChange = (page: number, limit: number) => {
  120. setSearchParams(prev => ({ ...prev, page, limit }));
  121. };
  122. // 处理过滤条件变化
  123. const handleFilterChange = (newFilters: Partial<typeof filters>) => {
  124. setFilters(prev => ({ ...prev, ...newFilters }));
  125. setSearchParams(prev => ({ ...prev, page: 1 }));
  126. };
  127. // 重置所有过滤条件
  128. const resetFilters = () => {
  129. setFilters({
  130. status: undefined,
  131. createdAt: undefined
  132. });
  133. setSearchParams(prev => ({ ...prev, page: 1 }));
  134. };
  135. // 检查是否有活跃的过滤条件
  136. const hasActiveFilters = useMemo(() => {
  137. return filters.status !== undefined ||
  138. filters.createdAt !== undefined;
  139. }, [filters]);
  140. // 打开创建租户对话框
  141. const handleCreateTenant = () => {
  142. setEditingTenant(null);
  143. setIsCreateForm(true);
  144. createForm.reset({
  145. name: '',
  146. code: '',
  147. phone: null,
  148. contactName: null,
  149. status: 1,
  150. config: null,
  151. rsaPublicKey: null,
  152. aesKey: null,
  153. });
  154. setIsModalOpen(true);
  155. };
  156. // 打开编辑租户对话框
  157. const handleEditTenant = (tenant: TenantResponse) => {
  158. setEditingTenant(tenant);
  159. setIsCreateForm(false);
  160. updateForm.reset({
  161. name: tenant.name,
  162. code: tenant.code,
  163. phone: tenant.phone,
  164. contactName: tenant.contactName,
  165. status: tenant.status,
  166. config: tenant.config,
  167. rsaPublicKey: tenant.rsaPublicKey,
  168. aesKey: tenant.aesKey,
  169. });
  170. setIsModalOpen(true);
  171. };
  172. // 处理创建表单提交
  173. const handleCreateSubmit = async (data: CreateTenantFormData) => {
  174. try {
  175. const res = await tenantClient.index.$post({
  176. json: data
  177. });
  178. if (res.status !== 201) {
  179. throw new Error('创建租户失败');
  180. }
  181. toast.success('租户创建成功');
  182. setIsModalOpen(false);
  183. refetch();
  184. } catch {
  185. toast.error('创建失败,请重试');
  186. }
  187. };
  188. // 处理更新表单提交
  189. const handleUpdateSubmit = async (data: UpdateTenantFormData) => {
  190. if (!editingTenant) return;
  191. try {
  192. const res = await tenantClient[':id']['$put']({
  193. param: { id: editingTenant.id },
  194. json: data
  195. });
  196. if (res.status !== 200) {
  197. throw new Error('更新租户失败');
  198. }
  199. toast.success('租户更新成功');
  200. setIsModalOpen(false);
  201. refetch();
  202. } catch {
  203. toast.error('更新失败,请重试');
  204. }
  205. };
  206. // 处理删除租户
  207. const handleDeleteTenant = (id: number) => {
  208. setTenantToDelete(id);
  209. setDeleteDialogOpen(true);
  210. };
  211. const confirmDelete = async () => {
  212. if (!tenantToDelete) return;
  213. try {
  214. const res = await tenantClient[':id']['$delete']({
  215. param: { id: tenantToDelete }
  216. });
  217. if (res.status !== 204) {
  218. throw new Error('删除租户失败');
  219. }
  220. toast.success('租户删除成功');
  221. refetch();
  222. } catch {
  223. toast.error('删除失败,请重试');
  224. } finally {
  225. setDeleteDialogOpen(false);
  226. setTenantToDelete(null);
  227. }
  228. };
  229. // 渲染表格部分的骨架屏
  230. const renderTableSkeleton = () => (
  231. <div className="space-y-2">
  232. {Array.from({ length: 5 }).map((_, index) => (
  233. <div key={index} className="flex space-x-4">
  234. <Skeleton className="h-4 flex-1" />
  235. <Skeleton className="h-4 flex-1" />
  236. <Skeleton className="h-4 flex-1" />
  237. <Skeleton className="h-4 flex-1" />
  238. <Skeleton className="h-4 flex-1" />
  239. <Skeleton className="h-4 flex-1" />
  240. <Skeleton className="h-4 w-16" />
  241. </div>
  242. ))}
  243. </div>
  244. );
  245. return (
  246. <div className="space-y-4">
  247. <div className="flex justify-between items-center">
  248. <h1 className="text-2xl font-bold">租户管理</h1>
  249. <Button onClick={handleCreateTenant}>
  250. <Plus className="mr-2 h-4 w-4" />
  251. 创建租户
  252. </Button>
  253. </div>
  254. <Card>
  255. <CardHeader>
  256. <CardTitle>租户列表</CardTitle>
  257. <CardDescription>
  258. 管理系统中的所有租户,共 {totalCount} 个租户
  259. </CardDescription>
  260. </CardHeader>
  261. <CardContent>
  262. <div className="mb-4 space-y-4">
  263. <form onSubmit={handleSearch} className="flex gap-2">
  264. <div className="relative flex-1 max-w-sm">
  265. <Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
  266. <Input
  267. placeholder="搜索租户名称、代码或联系人..."
  268. value={searchParams.keyword}
  269. onChange={handleSearchChange}
  270. className="pl-8"
  271. />
  272. </div>
  273. <Button type="submit" variant="outline">
  274. 搜索
  275. </Button>
  276. <Button
  277. type="button"
  278. variant="outline"
  279. onClick={() => setShowFilters(!showFilters)}
  280. className="flex items-center gap-2"
  281. >
  282. <Filter className="h-4 w-4" />
  283. 高级筛选
  284. {hasActiveFilters && (
  285. <Badge variant="secondary" className="ml-1">
  286. {Object.values(filters).filter(v =>
  287. v !== undefined &&
  288. (!Array.isArray(v) || v.length > 0)
  289. ).length}
  290. </Badge>
  291. )}
  292. </Button>
  293. {hasActiveFilters && (
  294. <Button
  295. type="button"
  296. variant="ghost"
  297. onClick={resetFilters}
  298. className="flex items-center gap-2"
  299. >
  300. <X className="h-4 w-4" />
  301. 重置
  302. </Button>
  303. )}
  304. </form>
  305. {showFilters && (
  306. <div className="grid grid-cols-1 md:grid-cols-2 gap-4 p-4 border rounded-lg bg-muted/50">
  307. {/* 状态筛选 */}
  308. <div className="space-y-2">
  309. <label className="text-sm font-medium">租户状态</label>
  310. <Select
  311. value={filters.status === undefined ? 'all' : filters.status.toString()}
  312. onValueChange={(value) =>
  313. handleFilterChange({
  314. status: value === 'all' ? undefined : parseInt(value)
  315. })
  316. }
  317. >
  318. <SelectTrigger>
  319. <SelectValue placeholder="选择状态" />
  320. </SelectTrigger>
  321. <SelectContent>
  322. <SelectItem value="all">全部状态</SelectItem>
  323. <SelectItem value="1">启用</SelectItem>
  324. <SelectItem value="2">禁用</SelectItem>
  325. </SelectContent>
  326. </Select>
  327. </div>
  328. {/* 创建时间筛选 */}
  329. <div className="space-y-2">
  330. <label className="text-sm font-medium">创建时间</label>
  331. <Popover>
  332. <PopoverTrigger asChild>
  333. <Button
  334. variant="outline"
  335. className={cn(
  336. "w-full justify-start text-left font-normal",
  337. !filters.createdAt && "text-muted-foreground"
  338. )}
  339. >
  340. {filters.createdAt ?
  341. `${filters.createdAt.gte || ''} 至 ${filters.createdAt.lte || ''}` :
  342. '选择日期范围'
  343. }
  344. </Button>
  345. </PopoverTrigger>
  346. <PopoverContent className="w-auto p-0" align="start">
  347. <Calendar
  348. mode="range"
  349. selected={{
  350. from: filters.createdAt?.gte ? new Date(filters.createdAt.gte) : undefined,
  351. to: filters.createdAt?.lte ? new Date(filters.createdAt.lte) : undefined
  352. }}
  353. onSelect={(range) => {
  354. handleFilterChange({
  355. createdAt: range?.from && range?.to ? {
  356. gte: format(range.from, 'yyyy-MM-dd'),
  357. lte: format(range.to, 'yyyy-MM-dd')
  358. } : undefined
  359. });
  360. }}
  361. initialFocus
  362. />
  363. </PopoverContent>
  364. </Popover>
  365. </div>
  366. </div>
  367. )}
  368. {/* 过滤条件标签 */}
  369. {hasActiveFilters && (
  370. <div className="flex flex-wrap gap-2">
  371. {filters.status !== undefined && (
  372. <Badge variant="secondary" className="flex items-center gap-1">
  373. 状态: {filters.status === 1 ? '启用' : '禁用'}
  374. <X
  375. className="h-3 w-3 cursor-pointer"
  376. onClick={() => handleFilterChange({ status: undefined })}
  377. />
  378. </Badge>
  379. )}
  380. {filters.createdAt && (
  381. <Badge variant="secondary" className="flex items-center gap-1">
  382. 创建时间: {filters.createdAt.gte || ''} 至 {filters.createdAt.lte || ''}
  383. <X
  384. className="h-3 w-3 cursor-pointer"
  385. onClick={() => handleFilterChange({ createdAt: undefined })}
  386. />
  387. </Badge>
  388. )}
  389. </div>
  390. )}
  391. </div>
  392. <div className="rounded-md border">
  393. <Table>
  394. <TableHeader>
  395. <TableRow>
  396. <TableHead>租户名称</TableHead>
  397. <TableHead>租户代码</TableHead>
  398. <TableHead>联系人</TableHead>
  399. <TableHead>联系电话</TableHead>
  400. <TableHead>状态</TableHead>
  401. <TableHead>创建时间</TableHead>
  402. <TableHead className="text-right">操作</TableHead>
  403. </TableRow>
  404. </TableHeader>
  405. <TableBody>
  406. {isLoading ? (
  407. // 显示表格骨架屏
  408. <TableRow>
  409. <TableCell colSpan={7} className="p-4">
  410. {renderTableSkeleton()}
  411. </TableCell>
  412. </TableRow>
  413. ) : (
  414. // 显示实际租户数据
  415. tenants.map((tenant) => {
  416. const statusInfo = formatTenantStatus(tenant.status);
  417. return (
  418. <TableRow key={tenant.id}>
  419. <TableCell className="font-medium">{tenant.name || '-'}</TableCell>
  420. <TableCell>{tenant.code}</TableCell>
  421. <TableCell>{tenant.contactName || '-'}</TableCell>
  422. <TableCell>{tenant.phone || '-'}</TableCell>
  423. <TableCell>
  424. <Badge variant={statusInfo.variant}>
  425. {statusInfo.label}
  426. </Badge>
  427. </TableCell>
  428. <TableCell>
  429. {format(new Date(tenant.createdAt), 'yyyy-MM-dd HH:mm')}
  430. </TableCell>
  431. <TableCell className="text-right">
  432. <div className="flex justify-end gap-2">
  433. <Button
  434. variant="ghost"
  435. size="icon"
  436. onClick={() => handleEditTenant(tenant)}
  437. >
  438. <Edit className="h-4 w-4" />
  439. </Button>
  440. <Button
  441. variant="ghost"
  442. size="icon"
  443. onClick={() => handleDeleteTenant(tenant.id)}
  444. >
  445. <Trash2 className="h-4 w-4" />
  446. </Button>
  447. </div>
  448. </TableCell>
  449. </TableRow>
  450. );
  451. })
  452. )}
  453. </TableBody>
  454. </Table>
  455. </div>
  456. <DataTablePagination
  457. currentPage={searchParams.page}
  458. totalCount={totalCount}
  459. pageSize={searchParams.limit}
  460. onPageChange={handlePageChange}
  461. />
  462. </CardContent>
  463. </Card>
  464. {/* 创建/编辑租户对话框 */}
  465. <Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
  466. <DialogContent className="sm:max-w-[500px] max-h-[90vh] overflow-y-auto">
  467. <DialogHeader>
  468. <DialogTitle>
  469. {editingTenant ? '编辑租户' : '创建租户'}
  470. </DialogTitle>
  471. <DialogDescription>
  472. {editingTenant ? '编辑现有租户信息' : '创建一个新的租户'}
  473. </DialogDescription>
  474. </DialogHeader>
  475. {isCreateForm ? (
  476. <Form {...createForm}>
  477. <form onSubmit={createForm.handleSubmit(handleCreateSubmit)} className="space-y-4">
  478. <FormField
  479. control={createForm.control}
  480. name="name"
  481. render={({ field }) => (
  482. <FormItem>
  483. <FormLabel>租户名称</FormLabel>
  484. <FormControl>
  485. <Input placeholder="请输入租户名称" {...field} />
  486. </FormControl>
  487. <FormMessage />
  488. </FormItem>
  489. )}
  490. />
  491. <FormField
  492. control={createForm.control}
  493. name="code"
  494. render={({ field }) => (
  495. <FormItem>
  496. <FormLabel className="flex items-center">
  497. 租户代码
  498. <span className="text-red-500 ml-1">*</span>
  499. </FormLabel>
  500. <FormControl>
  501. <Input placeholder="请输入租户代码" {...field} />
  502. </FormControl>
  503. <FormMessage />
  504. </FormItem>
  505. )}
  506. />
  507. <FormField
  508. control={createForm.control}
  509. name="contactName"
  510. render={({ field }) => (
  511. <FormItem>
  512. <FormLabel>联系人姓名</FormLabel>
  513. <FormControl>
  514. <Input placeholder="请输入联系人姓名" {...field} />
  515. </FormControl>
  516. <FormMessage />
  517. </FormItem>
  518. )}
  519. />
  520. <FormField
  521. control={createForm.control}
  522. name="phone"
  523. render={({ field }) => (
  524. <FormItem>
  525. <FormLabel>联系电话</FormLabel>
  526. <FormControl>
  527. <Input placeholder="请输入联系电话" {...field} />
  528. </FormControl>
  529. <FormMessage />
  530. </FormItem>
  531. )}
  532. />
  533. <FormField
  534. control={createForm.control}
  535. name="status"
  536. render={({ field }) => (
  537. <FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
  538. <div className="space-y-0.5">
  539. <FormLabel className="text-base">租户状态</FormLabel>
  540. <FormDescription>
  541. 禁用后租户将无法使用系统
  542. </FormDescription>
  543. </div>
  544. <FormControl>
  545. <Switch
  546. checked={field.value === 1}
  547. onCheckedChange={(checked) => field.onChange(checked ? 1 : 2)}
  548. />
  549. </FormControl>
  550. </FormItem>
  551. )}
  552. />
  553. <DialogFooter>
  554. <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
  555. 取消
  556. </Button>
  557. <Button type="submit">
  558. 创建租户
  559. </Button>
  560. </DialogFooter>
  561. </form>
  562. </Form>
  563. ) : (
  564. <Form {...updateForm}>
  565. <form onSubmit={updateForm.handleSubmit(handleUpdateSubmit)} className="space-y-4">
  566. <FormField
  567. control={updateForm.control}
  568. name="name"
  569. render={({ field }) => (
  570. <FormItem>
  571. <FormLabel>租户名称</FormLabel>
  572. <FormControl>
  573. <Input placeholder="请输入租户名称" {...field} />
  574. </FormControl>
  575. <FormMessage />
  576. </FormItem>
  577. )}
  578. />
  579. <FormField
  580. control={updateForm.control}
  581. name="code"
  582. render={({ field }) => (
  583. <FormItem>
  584. <FormLabel className="flex items-center">
  585. 租户代码
  586. <span className="text-red-500 ml-1">*</span>
  587. </FormLabel>
  588. <FormControl>
  589. <Input placeholder="请输入租户代码" {...field} />
  590. </FormControl>
  591. <FormMessage />
  592. </FormItem>
  593. )}
  594. />
  595. <FormField
  596. control={updateForm.control}
  597. name="contactName"
  598. render={({ field }) => (
  599. <FormItem>
  600. <FormLabel>联系人姓名</FormLabel>
  601. <FormControl>
  602. <Input placeholder="请输入联系人姓名" {...field} />
  603. </FormControl>
  604. <FormMessage />
  605. </FormItem>
  606. )}
  607. />
  608. <FormField
  609. control={updateForm.control}
  610. name="phone"
  611. render={({ field }) => (
  612. <FormItem>
  613. <FormLabel>联系电话</FormLabel>
  614. <FormControl>
  615. <Input placeholder="请输入联系电话" {...field} />
  616. </FormControl>
  617. <FormMessage />
  618. </FormItem>
  619. )}
  620. />
  621. <FormField
  622. control={updateForm.control}
  623. name="status"
  624. render={({ field }) => (
  625. <FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
  626. <div className="space-y-0.5">
  627. <FormLabel className="text-base">租户状态</FormLabel>
  628. <FormDescription>
  629. 禁用后租户将无法使用系统
  630. </FormDescription>
  631. </div>
  632. <FormControl>
  633. <Switch
  634. checked={field.value === 1}
  635. onCheckedChange={(checked) => field.onChange(checked ? 1 : 2)}
  636. />
  637. </FormControl>
  638. </FormItem>
  639. )}
  640. />
  641. <DialogFooter>
  642. <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
  643. 取消
  644. </Button>
  645. <Button type="submit">
  646. 更新租户
  647. </Button>
  648. </DialogFooter>
  649. </form>
  650. </Form>
  651. )}
  652. </DialogContent>
  653. </Dialog>
  654. {/* 删除确认对话框 */}
  655. <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
  656. <DialogContent>
  657. <DialogHeader>
  658. <DialogTitle>确认删除</DialogTitle>
  659. <DialogDescription>
  660. 确定要删除这个租户吗?此操作无法撤销。
  661. </DialogDescription>
  662. </DialogHeader>
  663. <DialogFooter>
  664. <Button variant="outline" onClick={() => setDeleteDialogOpen(false)}>
  665. 取消
  666. </Button>
  667. <Button variant="destructive" onClick={confirmDelete}>
  668. 删除
  669. </Button>
  670. </DialogFooter>
  671. </DialogContent>
  672. </Dialog>
  673. </div>
  674. );
  675. };