Goods.tsx 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706
  1. import React, { useState } from 'react';
  2. import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
  3. import { format } from 'date-fns';
  4. import { zhCN } from 'date-fns/locale';
  5. import { toast } from 'sonner';
  6. import { zodResolver } from '@hookform/resolvers/zod';
  7. import { useForm } from 'react-hook-form';
  8. import type { InferRequestType, InferResponseType } from 'hono/client';
  9. import { Button } from '@/client/components/ui/button';
  10. import { Input } from '@/client/components/ui/input';
  11. import { Label } from '@/client/components/ui/label';
  12. import { Badge } from '@/client/components/ui/badge';
  13. import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/client/components/ui/card';
  14. import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/client/components/ui/table';
  15. import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/client/components/ui/dialog';
  16. import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/client/components/ui/form';
  17. import { Textarea } from '@/client/components/ui/textarea';
  18. import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/client/components/ui/select';
  19. import { goodsClient } from '@/client/api';
  20. import { CreateGoodsDto, UpdateGoodsDto } from '@/server/modules/goods/goods.schema';
  21. import { DataTablePagination } from '@/client/admin-shadcn/components/DataTablePagination';
  22. import ImageSelector from '@/client/admin-shadcn/components/ImageSelector';
  23. import GoodsCategorySelector from '@/client/admin-shadcn/components/GoodsCategorySelector';
  24. import GoodsCategoryCascadeSelector from '@/client/admin-shadcn/components/GoodsCategoryCascadeSelector';
  25. import SupplierSelector from '@/client/admin-shadcn/components/SupplierSelector';
  26. import { Search, Plus, Edit, Trash2, Package } from 'lucide-react';
  27. type CreateRequest = InferRequestType<typeof goodsClient.$post>['json'];
  28. type UpdateRequest = InferRequestType<typeof goodsClient[':id']['$put']>['json'];
  29. type GoodsResponse = InferResponseType<typeof goodsClient.$get, 200>['data'][0];
  30. const createFormSchema = CreateGoodsDto;
  31. const updateFormSchema = UpdateGoodsDto;
  32. export const GoodsPage = () => {
  33. const queryClient = useQueryClient();
  34. const [searchParams, setSearchParams] = useState({ page: 1, limit: 10, search: '' });
  35. const [isModalOpen, setIsModalOpen] = useState(false);
  36. const [editingGoods, setEditingGoods] = useState<GoodsResponse | null>(null);
  37. const [isCreateForm, setIsCreateForm] = useState(true);
  38. const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
  39. const [goodsToDelete, setGoodsToDelete] = useState<number | null>(null);
  40. // 创建表单
  41. const createForm = useForm<CreateRequest>({
  42. resolver: zodResolver(createFormSchema),
  43. defaultValues: {
  44. name: '',
  45. price: 0,
  46. costPrice: 0,
  47. categoryId1: 0,
  48. categoryId2: 0,
  49. categoryId3: 0,
  50. goodsType: 1,
  51. supplierId: null,
  52. imageFileId: null,
  53. slideImageIds: [],
  54. detail: '',
  55. instructions: '',
  56. sort: 0,
  57. state: 1,
  58. stock: 0,
  59. lowestBuy: 1,
  60. },
  61. });
  62. // 更新表单
  63. const updateForm = useForm<UpdateRequest>({
  64. resolver: zodResolver(updateFormSchema),
  65. });
  66. // 获取商品列表
  67. const { data, isLoading, refetch } = useQuery({
  68. queryKey: ['goods', searchParams],
  69. queryFn: async () => {
  70. const res = await goodsClient.$get({
  71. query: {
  72. page: searchParams.page,
  73. pageSize: searchParams.limit,
  74. keyword: searchParams.search,
  75. }
  76. });
  77. if (res.status !== 200) throw new Error('获取商品列表失败');
  78. return await res.json();
  79. }
  80. });
  81. // 创建商品
  82. const createMutation = useMutation({
  83. mutationFn: async (data: CreateRequest) => {
  84. const res = await goodsClient.$post({ json: data });
  85. if (res.status !== 201) throw new Error('创建商品失败');
  86. return await res.json();
  87. },
  88. onSuccess: () => {
  89. toast.success('商品创建成功');
  90. setIsModalOpen(false);
  91. createForm.reset();
  92. refetch();
  93. },
  94. onError: (error) => {
  95. toast.error(error.message || '创建商品失败');
  96. }
  97. });
  98. // 更新商品
  99. const updateMutation = useMutation({
  100. mutationFn: async ({ id, data }: { id: number; data: UpdateRequest }) => {
  101. const res = await goodsClient[':id']['$put']({
  102. param: { id: id.toString() },
  103. json: data
  104. });
  105. if (res.status !== 200) throw new Error('更新商品失败');
  106. return await res.json();
  107. },
  108. onSuccess: () => {
  109. toast.success('商品更新成功');
  110. setIsModalOpen(false);
  111. setEditingGoods(null);
  112. refetch();
  113. },
  114. onError: (error) => {
  115. toast.error(error.message || '更新商品失败');
  116. }
  117. });
  118. // 删除商品
  119. const deleteMutation = useMutation({
  120. mutationFn: async (id: number) => {
  121. const res = await goodsClient[':id']['$delete']({
  122. param: { id: id.toString() }
  123. });
  124. if (res.status !== 204) throw new Error('删除商品失败');
  125. return id;
  126. },
  127. onSuccess: () => {
  128. toast.success('商品删除成功');
  129. setDeleteDialogOpen(false);
  130. setGoodsToDelete(null);
  131. refetch();
  132. },
  133. onError: (error) => {
  134. toast.error(error.message || '删除商品失败');
  135. }
  136. });
  137. // 处理搜索
  138. const handleSearch = (e: React.FormEvent) => {
  139. e.preventDefault();
  140. setSearchParams(prev => ({ ...prev, page: 1 }));
  141. };
  142. // 处理创建
  143. const handleCreateGoods = () => {
  144. setIsCreateForm(true);
  145. setEditingGoods(null);
  146. createForm.reset();
  147. setIsModalOpen(true);
  148. };
  149. // 处理编辑
  150. const handleEditGoods = (goods: GoodsResponse) => {
  151. setIsCreateForm(false);
  152. setEditingGoods(goods);
  153. updateForm.reset({
  154. name: goods.name,
  155. price: goods.price,
  156. costPrice: goods.costPrice,
  157. categoryId1: goods.categoryId1,
  158. categoryId2: goods.categoryId2,
  159. categoryId3: goods.categoryId3,
  160. goodsType: goods.goodsType,
  161. supplierId: goods.supplierId,
  162. imageFileId: goods.imageFileId,
  163. slideImageIds: goods.slideImages?.map(img => img.id) || [],
  164. detail: goods.detail || '',
  165. instructions: goods.instructions || '',
  166. sort: goods.sort,
  167. state: goods.state,
  168. stock: goods.stock,
  169. lowestBuy: goods.lowestBuy,
  170. });
  171. setIsModalOpen(true);
  172. };
  173. // 处理删除
  174. const handleDeleteGoods = (id: number) => {
  175. setGoodsToDelete(id);
  176. setDeleteDialogOpen(true);
  177. };
  178. // 确认删除
  179. const confirmDelete = () => {
  180. if (goodsToDelete) {
  181. deleteMutation.mutate(goodsToDelete);
  182. }
  183. };
  184. // 提交表单
  185. const handleSubmit = (data: CreateRequest | UpdateRequest) => {
  186. if (isCreateForm) {
  187. createMutation.mutate(data as CreateRequest);
  188. } else if (editingGoods) {
  189. updateMutation.mutate({ id: editingGoods.id, data: data as UpdateRequest });
  190. }
  191. };
  192. return (
  193. <div className="space-y-4">
  194. <div className="flex justify-between items-center">
  195. <h1 className="text-2xl font-bold">商品管理</h1>
  196. <Button onClick={handleCreateGoods}>
  197. <Plus className="mr-2 h-4 w-4" />
  198. 创建商品
  199. </Button>
  200. </div>
  201. <Card>
  202. <CardHeader>
  203. <CardTitle>商品列表</CardTitle>
  204. <CardDescription>管理您的商品信息</CardDescription>
  205. </CardHeader>
  206. <CardContent>
  207. <form onSubmit={handleSearch} className="mb-4">
  208. <div className="flex gap-2">
  209. <div className="relative flex-1 max-w-sm">
  210. <Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
  211. <Input
  212. placeholder="搜索商品名称..."
  213. value={searchParams.search}
  214. onChange={(e) => setSearchParams(prev => ({ ...prev, search: e.target.value }))}
  215. className="pl-8"
  216. />
  217. </div>
  218. <Button type="submit" variant="outline">
  219. 搜索
  220. </Button>
  221. </div>
  222. </form>
  223. <div className="rounded-md border">
  224. <Table>
  225. <TableHeader>
  226. <TableRow>
  227. <TableHead>商品图片</TableHead>
  228. <TableHead>商品名称</TableHead>
  229. <TableHead>价格</TableHead>
  230. <TableHead>库存</TableHead>
  231. <TableHead>销量</TableHead>
  232. <TableHead>供应商</TableHead>
  233. <TableHead>状态</TableHead>
  234. <TableHead>创建时间</TableHead>
  235. <TableHead className="text-right">操作</TableHead>
  236. </TableRow>
  237. </TableHeader>
  238. <TableBody>
  239. {data?.data.map((goods) => (
  240. <TableRow key={goods.id}>
  241. <TableCell>
  242. {goods.imageFile?.fullUrl ? (
  243. <img
  244. src={goods.imageFile.fullUrl}
  245. alt={goods.name}
  246. className="w-12 h-12 object-cover rounded"
  247. />
  248. ) : (
  249. <div className="w-12 h-12 bg-gray-200 rounded flex items-center justify-center">
  250. <Package className="h-6 w-6 text-gray-400" />
  251. </div>
  252. )}
  253. </TableCell>
  254. <TableCell className="font-medium">{goods.name}</TableCell>
  255. <TableCell>¥{goods.price.toFixed(2)}</TableCell>
  256. <TableCell>{goods.stock}</TableCell>
  257. <TableCell>{goods.salesNum}</TableCell>
  258. <TableCell>{goods.supplier?.name || '-'}</TableCell>
  259. <TableCell>
  260. <Badge variant={goods.state === 1 ? 'default' : 'secondary'}>
  261. {goods.state === 1 ? '可用' : '不可用'}
  262. </Badge>
  263. </TableCell>
  264. <TableCell>
  265. {format(new Date(goods.createdAt), 'yyyy-MM-dd', { locale: zhCN })}
  266. </TableCell>
  267. <TableCell className="text-right">
  268. <div className="flex justify-end gap-2">
  269. <Button
  270. variant="ghost"
  271. size="icon"
  272. onClick={() => handleEditGoods(goods)}
  273. >
  274. <Edit className="h-4 w-4" />
  275. </Button>
  276. <Button
  277. variant="ghost"
  278. size="icon"
  279. onClick={() => handleDeleteGoods(goods.id)}
  280. >
  281. <Trash2 className="h-4 w-4" />
  282. </Button>
  283. </div>
  284. </TableCell>
  285. </TableRow>
  286. ))}
  287. </TableBody>
  288. </Table>
  289. {data?.data.length === 0 && !isLoading && (
  290. <div className="text-center py-8">
  291. <p className="text-muted-foreground">暂无商品数据</p>
  292. </div>
  293. )}
  294. </div>
  295. <DataTablePagination
  296. currentPage={searchParams.page}
  297. pageSize={searchParams.limit}
  298. totalCount={data?.pagination.total || 0}
  299. onPageChange={(page, limit) => setSearchParams(prev => ({ ...prev, page, limit }))}
  300. />
  301. </CardContent>
  302. </Card>
  303. {/* 创建/编辑对话框 */}
  304. <Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
  305. <DialogContent className="sm:max-w-[700px] max-h-[90vh] overflow-y-auto">
  306. <DialogHeader>
  307. <DialogTitle>{isCreateForm ? '创建商品' : '编辑商品'}</DialogTitle>
  308. <DialogDescription>
  309. {isCreateForm ? '创建一个新的商品' : '编辑商品信息'}
  310. </DialogDescription>
  311. </DialogHeader>
  312. {isCreateForm ? (
  313. <Form {...createForm}>
  314. <form onSubmit={createForm.handleSubmit(handleSubmit)} className="space-y-4">
  315. <FormField
  316. control={createForm.control}
  317. name="name"
  318. render={({ field }) => (
  319. <FormItem>
  320. <FormLabel>商品名称 <span className="text-red-500">*</span></FormLabel>
  321. <FormControl>
  322. <Input placeholder="请输入商品名称" {...field} />
  323. </FormControl>
  324. <FormMessage />
  325. </FormItem>
  326. )}
  327. />
  328. <div className="grid grid-cols-2 gap-4">
  329. <FormField
  330. control={createForm.control}
  331. name="price"
  332. render={({ field }) => (
  333. <FormItem>
  334. <FormLabel>售卖价 <span className="text-red-500">*</span></FormLabel>
  335. <FormControl>
  336. <Input type="number" step="0.01" placeholder="0.00" {...field} />
  337. </FormControl>
  338. <FormMessage />
  339. </FormItem>
  340. )}
  341. />
  342. <FormField
  343. control={createForm.control}
  344. name="costPrice"
  345. render={({ field }) => (
  346. <FormItem>
  347. <FormLabel>成本价 <span className="text-red-500">*</span></FormLabel>
  348. <FormControl>
  349. <Input type="number" step="0.01" placeholder="0.00" {...field} />
  350. </FormControl>
  351. <FormMessage />
  352. </FormItem>
  353. )}
  354. />
  355. </div>
  356. <GoodsCategoryCascadeSelector required={true} />
  357. <div className="grid grid-cols-2 gap-4">
  358. <FormField
  359. control={createForm.control}
  360. name="supplierId"
  361. render={({ field }) => (
  362. <FormItem>
  363. <FormLabel>供应商</FormLabel>
  364. <FormControl>
  365. <SupplierSelector
  366. value={field.value || undefined}
  367. onChange={field.onChange}
  368. />
  369. </FormControl>
  370. <FormMessage />
  371. </FormItem>
  372. )}
  373. />
  374. <FormField
  375. control={createForm.control}
  376. name="goodsType"
  377. render={({ field }) => (
  378. <FormItem>
  379. <FormLabel>商品类型</FormLabel>
  380. <Select
  381. value={field.value?.toString()}
  382. onValueChange={(value) => field.onChange(parseInt(value))}
  383. >
  384. <FormControl>
  385. <SelectTrigger>
  386. <SelectValue placeholder="选择商品类型" />
  387. </SelectTrigger>
  388. </FormControl>
  389. <SelectContent>
  390. <SelectItem value="1">实物产品</SelectItem>
  391. <SelectItem value="2">虚拟产品</SelectItem>
  392. </SelectContent>
  393. </Select>
  394. <FormMessage />
  395. </FormItem>
  396. )}
  397. />
  398. </div>
  399. <FormField
  400. control={createForm.control}
  401. name="stock"
  402. render={({ field }) => (
  403. <FormItem>
  404. <FormLabel>库存 <span className="text-red-500">*</span></FormLabel>
  405. <FormControl>
  406. <Input type="number" placeholder="0" {...field} />
  407. </FormControl>
  408. <FormMessage />
  409. </FormItem>
  410. )}
  411. />
  412. <FormField
  413. control={createForm.control}
  414. name="imageFileId"
  415. render={({ field }) => (
  416. <FormItem>
  417. <FormLabel>商品主图</FormLabel>
  418. <FormControl>
  419. <ImageSelector
  420. value={field.value || undefined}
  421. onChange={field.onChange}
  422. maxSize={2}
  423. uploadPath="/goods"
  424. uploadButtonText="上传商品主图"
  425. previewSize="medium"
  426. placeholder="选择商品主图"
  427. />
  428. </FormControl>
  429. <FormDescription>推荐尺寸:800x800px</FormDescription>
  430. <FormMessage />
  431. </FormItem>
  432. )}
  433. />
  434. <FormField
  435. control={createForm.control}
  436. name="slideImageIds"
  437. render={({ field }) => (
  438. <FormItem>
  439. <FormLabel>商品轮播图</FormLabel>
  440. <FormControl>
  441. <ImageSelector
  442. value={field.value || []}
  443. onChange={field.onChange}
  444. allowMultiple={true}
  445. maxSize={5}
  446. uploadPath="/goods/slide"
  447. uploadButtonText="上传轮播图"
  448. previewSize="small"
  449. placeholder="选择商品轮播图"
  450. accept="image/*"
  451. />
  452. </FormControl>
  453. <FormDescription>最多上传5张轮播图,推荐尺寸:800x800px</FormDescription>
  454. <FormMessage />
  455. </FormItem>
  456. )}
  457. />
  458. <FormField
  459. control={createForm.control}
  460. name="instructions"
  461. render={({ field }) => (
  462. <FormItem>
  463. <FormLabel>商品简介</FormLabel>
  464. <FormControl>
  465. <Textarea
  466. placeholder="请输入商品简介"
  467. className="resize-none"
  468. {...field}
  469. />
  470. </FormControl>
  471. <FormMessage />
  472. </FormItem>
  473. )}
  474. />
  475. <DialogFooter>
  476. <Button
  477. type="button"
  478. variant="outline"
  479. onClick={() => setIsModalOpen(false)}
  480. >
  481. 取消
  482. </Button>
  483. <Button type="submit" disabled={createMutation.isPending}>
  484. {createMutation.isPending ? '创建中...' : '创建'}
  485. </Button>
  486. </DialogFooter>
  487. </form>
  488. </Form>
  489. ) : (
  490. <Form {...updateForm}>
  491. <form onSubmit={updateForm.handleSubmit(handleSubmit)} className="space-y-4">
  492. <FormField
  493. control={updateForm.control}
  494. name="name"
  495. render={({ field }) => (
  496. <FormItem>
  497. <FormLabel>商品名称 <span className="text-red-500">*</span></FormLabel>
  498. <FormControl>
  499. <Input placeholder="请输入商品名称" {...field} />
  500. </FormControl>
  501. <FormMessage />
  502. </FormItem>
  503. )}
  504. />
  505. <div className="grid grid-cols-2 gap-4">
  506. <FormField
  507. control={updateForm.control}
  508. name="price"
  509. render={({ field }) => (
  510. <FormItem>
  511. <FormLabel>售卖价</FormLabel>
  512. <FormControl>
  513. <Input type="number" step="0.01" {...field} />
  514. </FormControl>
  515. <FormMessage />
  516. </FormItem>
  517. )}
  518. />
  519. <FormField
  520. control={updateForm.control}
  521. name="costPrice"
  522. render={({ field }) => (
  523. <FormItem>
  524. <FormLabel>成本价</FormLabel>
  525. <FormControl>
  526. <Input type="number" step="0.01" {...field} />
  527. </FormControl>
  528. <FormMessage />
  529. </FormItem>
  530. )}
  531. />
  532. </div>
  533. <GoodsCategoryCascadeSelector />
  534. <div className="grid grid-cols-2 gap-4">
  535. <FormField
  536. control={updateForm.control}
  537. name="stock"
  538. render={({ field }) => (
  539. <FormItem>
  540. <FormLabel>库存</FormLabel>
  541. <FormControl>
  542. <Input type="number" {...field} />
  543. </FormControl>
  544. <FormMessage />
  545. </FormItem>
  546. )}
  547. />
  548. <FormField
  549. control={updateForm.control}
  550. name="imageFileId"
  551. render={({ field }) => (
  552. <FormItem>
  553. <FormLabel>商品主图</FormLabel>
  554. <FormControl>
  555. <ImageSelector
  556. value={field.value || undefined}
  557. onChange={field.onChange}
  558. maxSize={2}
  559. uploadPath="/goods"
  560. uploadButtonText="上传商品主图"
  561. previewSize="medium"
  562. placeholder="选择商品主图"
  563. />
  564. </FormControl>
  565. <FormDescription>推荐尺寸:800x800px</FormDescription>
  566. <FormMessage />
  567. </FormItem>
  568. )}
  569. />
  570. <FormField
  571. control={updateForm.control}
  572. name="slideImageIds"
  573. render={({ field }) => (
  574. <FormItem>
  575. <FormLabel>商品轮播图</FormLabel>
  576. <FormControl>
  577. <ImageSelector
  578. value={field.value || []}
  579. onChange={field.onChange}
  580. allowMultiple={true}
  581. maxSize={5}
  582. uploadPath="/goods/slide"
  583. uploadButtonText="上传轮播图"
  584. previewSize="small"
  585. placeholder="选择商品轮播图"
  586. accept="image/*"
  587. />
  588. </FormControl>
  589. <FormDescription>最多上传5张轮播图,推荐尺寸:800x800px</FormDescription>
  590. <FormMessage />
  591. </FormItem>
  592. )}
  593. />
  594. <FormField
  595. control={updateForm.control}
  596. name="state"
  597. render={({ field }) => (
  598. <FormItem>
  599. <FormLabel>状态</FormLabel>
  600. <Select
  601. value={field.value?.toString()}
  602. onValueChange={(value) => field.onChange(parseInt(value))}
  603. >
  604. <FormControl>
  605. <SelectTrigger>
  606. <SelectValue />
  607. </SelectTrigger>
  608. </FormControl>
  609. <SelectContent>
  610. <SelectItem value="1">可用</SelectItem>
  611. <SelectItem value="2">不可用</SelectItem>
  612. </SelectContent>
  613. </Select>
  614. <FormMessage />
  615. </FormItem>
  616. )}
  617. />
  618. </div>
  619. <DialogFooter>
  620. <Button
  621. type="button"
  622. variant="outline"
  623. onClick={() => setIsModalOpen(false)}
  624. >
  625. 取消
  626. </Button>
  627. <Button type="submit" disabled={updateMutation.isPending}>
  628. {updateMutation.isPending ? '更新中...' : '更新'}
  629. </Button>
  630. </DialogFooter>
  631. </form>
  632. </Form>
  633. )}
  634. </DialogContent>
  635. </Dialog>
  636. {/* 删除确认对话框 */}
  637. <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
  638. <DialogContent>
  639. <DialogHeader>
  640. <DialogTitle>确认删除</DialogTitle>
  641. <DialogDescription>
  642. 确定要删除这个商品吗?此操作无法撤销。
  643. </DialogDescription>
  644. </DialogHeader>
  645. <DialogFooter>
  646. <Button variant="outline" onClick={() => setDeleteDialogOpen(false)}>
  647. 取消
  648. </Button>
  649. <Button
  650. variant="destructive"
  651. onClick={confirmDelete}
  652. disabled={deleteMutation.isPending}
  653. >
  654. {deleteMutation.isPending ? '删除中...' : '删除'}
  655. </Button>
  656. </DialogFooter>
  657. </DialogContent>
  658. </Dialog>
  659. </div>
  660. );
  661. };