MerchantManagement.tsx 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711
  1. import { useState } from 'react'
  2. import { useQuery, useMutation } from '@tanstack/react-query'
  3. import { useForm } from 'react-hook-form'
  4. import { zodResolver } from '@hookform/resolvers/zod'
  5. import { format } from 'date-fns'
  6. import { zhCN } from 'date-fns/locale'
  7. import { toast } from 'sonner'
  8. import type { InferRequestType, InferResponseType } from 'hono/client'
  9. import { Plus, Search, Edit, Trash2, Eye } from 'lucide-react'
  10. import { Button } from '@d8d/shared-ui-components/components/ui/button'
  11. import { Input } from '@d8d/shared-ui-components/components/ui/input'
  12. import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@d8d/shared-ui-components/components/ui/card'
  13. import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@d8d/shared-ui-components/components/ui/table'
  14. import { Badge } from '@d8d/shared-ui-components/components/ui/badge'
  15. import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@d8d/shared-ui-components/components/ui/dialog'
  16. import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@d8d/shared-ui-components/components/ui/form'
  17. import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@d8d/shared-ui-components/components/ui/select'
  18. import { Skeleton } from '@d8d/shared-ui-components/components/ui/skeleton'
  19. import { DataTablePagination } from '@d8d/shared-ui-components/components/admin/DataTablePagination'
  20. import { merchantClient, merchantClientManager } from '../api/merchantClient'
  21. import { AdminCreateMerchantDto, AdminUpdateMerchantDto } from '@d8d/merchant-module/schemas'
  22. // 使用RPC方式提取类型
  23. type CreateMerchantRequest = InferRequestType<typeof merchantClient.index.$post>['json']
  24. type UpdateMerchantRequest = InferRequestType<typeof merchantClient[':id']['$put']>['json']
  25. type MerchantResponse = InferResponseType<typeof merchantClient.index.$get, 200>['data'][0]
  26. // 直接使用后端定义的 schema
  27. const createFormSchema = AdminCreateMerchantDto
  28. const updateFormSchema = AdminUpdateMerchantDto
  29. // 商户状态类型
  30. export enum MerchantState {
  31. ENABLED = 1,
  32. DISABLED = 2
  33. }
  34. // 商户状态映射
  35. export const MerchantStateMap = {
  36. [MerchantState.ENABLED]: '启用',
  37. [MerchantState.DISABLED]: '禁用'
  38. } as const
  39. // 商户状态徽章变体映射
  40. export const MerchantStateBadgeVariantMap = {
  41. [MerchantState.ENABLED]: 'default' as const,
  42. [MerchantState.DISABLED]: 'secondary' as const
  43. } as const
  44. // 搜索参数类型
  45. interface MerchantSearchParams {
  46. page: number
  47. limit: number
  48. search: string
  49. }
  50. export const MerchantManagement = () => {
  51. const [searchParams, setSearchParams] = useState<MerchantSearchParams>({
  52. page: 1,
  53. limit: 10,
  54. search: '',
  55. })
  56. const [isModalOpen, setIsModalOpen] = useState(false)
  57. const [editingMerchant, setEditingMerchant] = useState<MerchantResponse | null>(null)
  58. const [isCreateForm, setIsCreateForm] = useState(true)
  59. const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
  60. const [merchantToDelete, setMerchantToDelete] = useState<number | null>(null)
  61. const [detailDialogOpen, setDetailDialogOpen] = useState(false)
  62. const [detailMerchant, setDetailMerchant] = useState<MerchantResponse | null>(null)
  63. // 创建表单
  64. const createForm = useForm<CreateMerchantRequest>({
  65. resolver: zodResolver(createFormSchema),
  66. defaultValues: {
  67. name: '',
  68. username: '',
  69. password: '',
  70. phone: '',
  71. realname: '',
  72. state: 2,
  73. rsaPublicKey: '',
  74. aesKey: '',
  75. },
  76. })
  77. // 更新表单
  78. const updateForm = useForm<UpdateMerchantRequest>({
  79. resolver: zodResolver(updateFormSchema),
  80. })
  81. // 获取商户列表
  82. const { data, isLoading, refetch } = useQuery({
  83. queryKey: ['merchants', searchParams],
  84. queryFn: async () => {
  85. const res = await merchantClientManager.get().index.$get({
  86. query: {
  87. page: searchParams.page,
  88. pageSize: searchParams.limit,
  89. keyword: searchParams.search,
  90. }
  91. })
  92. if (res.status !== 200) throw new Error('获取商户列表失败')
  93. const result = await res.json()
  94. return result
  95. }
  96. })
  97. // 处理分页
  98. const handlePageChange = (page: number, pageSize: number) => {
  99. setSearchParams(prev => ({ ...prev, page, limit: pageSize }))
  100. }
  101. // 创建商户
  102. const createMutation = useMutation({
  103. mutationFn: async (data: CreateMerchantRequest) => {
  104. const res = await merchantClientManager.get().index.$post({ json: data })
  105. if (res.status !== 201) throw new Error('创建商户失败')
  106. return await res.json()
  107. },
  108. onSuccess: () => {
  109. toast.success('商户创建成功')
  110. setIsModalOpen(false)
  111. createForm.reset()
  112. refetch()
  113. },
  114. onError: (error: Error) => {
  115. toast.error(error.message || '创建失败')
  116. }
  117. })
  118. // 更新商户
  119. const updateMutation = useMutation({
  120. mutationFn: async ({ id, data }: { id: number; data: UpdateMerchantRequest }) => {
  121. const res = await merchantClientManager.get()[':id']['$put']({
  122. param: { id },
  123. json: data
  124. })
  125. if (res.status !== 200) throw new Error('更新商户失败')
  126. return await res.json()
  127. },
  128. onSuccess: () => {
  129. toast.success('商户更新成功')
  130. setIsModalOpen(false)
  131. setEditingMerchant(null)
  132. refetch()
  133. },
  134. onError: (error: Error) => {
  135. toast.error(error.message || '更新失败')
  136. }
  137. })
  138. // 删除商户
  139. const deleteMutation = useMutation({
  140. mutationFn: async (id: number) => {
  141. const res = await merchantClientManager.get()[':id']['$delete']({
  142. param: { id }
  143. })
  144. if (res.status !== 204) throw new Error('删除商户失败')
  145. return res
  146. },
  147. onSuccess: () => {
  148. toast.success('商户删除成功')
  149. setDeleteDialogOpen(false)
  150. setMerchantToDelete(null)
  151. refetch()
  152. },
  153. onError: (error: Error) => {
  154. toast.error(error.message || '删除失败')
  155. }
  156. })
  157. // 搜索处理
  158. const handleSearch = (e?: React.FormEvent) => {
  159. e?.preventDefault()
  160. setSearchParams(prev => ({ ...prev, page: 1 }))
  161. }
  162. // 创建商户
  163. const handleCreateMerchant = () => {
  164. setIsCreateForm(true)
  165. setEditingMerchant(null)
  166. createForm.reset()
  167. setIsModalOpen(true)
  168. }
  169. // 编辑商户
  170. const handleEditMerchant = (merchant: MerchantResponse) => {
  171. setIsCreateForm(false)
  172. setEditingMerchant(merchant)
  173. updateForm.reset({
  174. name: merchant.name || '',
  175. username: merchant.username,
  176. phone: merchant.phone || '',
  177. realname: merchant.realname || '',
  178. state: merchant.state,
  179. rsaPublicKey: merchant.rsaPublicKey || '',
  180. aesKey: merchant.aesKey || '',
  181. })
  182. setIsModalOpen(true)
  183. }
  184. // 查看详情
  185. const handleViewDetail = (merchant: MerchantResponse) => {
  186. setDetailMerchant(merchant)
  187. setDetailDialogOpen(true)
  188. }
  189. // 删除商户
  190. const handleDeleteMerchant = (id: number) => {
  191. setMerchantToDelete(id)
  192. setDeleteDialogOpen(true)
  193. }
  194. // 确认删除
  195. const confirmDelete = () => {
  196. if (merchantToDelete) {
  197. deleteMutation.mutate(merchantToDelete)
  198. }
  199. }
  200. // 提交表单
  201. const handleSubmit = (data: CreateMerchantRequest | UpdateMerchantRequest) => {
  202. if (isCreateForm) {
  203. createMutation.mutate(data as CreateMerchantRequest)
  204. } else if (editingMerchant) {
  205. updateMutation.mutate({ id: editingMerchant.id, data: data as UpdateMerchantRequest })
  206. }
  207. }
  208. // 状态文本
  209. const getStateText = (state: number) => {
  210. return MerchantStateMap[state as MerchantState] || '未知'
  211. }
  212. const getStateBadgeVariant = (state: number) => {
  213. return MerchantStateBadgeVariantMap[state as MerchantState] || 'secondary'
  214. }
  215. // 渲染加载骨架
  216. const renderSkeleton = () => (
  217. <Card>
  218. <CardContent className="pt-6">
  219. <div className="space-y-3">
  220. {[...Array(5)].map((_, i) => (
  221. <div key={i} className="flex gap-4">
  222. <Skeleton className="h-10 flex-1" />
  223. <Skeleton className="h-10 flex-1" />
  224. <Skeleton className="h-10 flex-1" />
  225. <Skeleton className="h-10 w-20" />
  226. </div>
  227. ))}
  228. </div>
  229. </CardContent>
  230. </Card>
  231. )
  232. return (
  233. <div className="space-y-4">
  234. {/* 页面标题 */}
  235. <div className="flex justify-between items-center">
  236. <h1 className="text-2xl font-bold">商户管理</h1>
  237. <Button onClick={handleCreateMerchant} data-testid="create-merchant-button">
  238. <Plus className="mr-2 h-4 w-4" />
  239. 创建商户
  240. </Button>
  241. </div>
  242. {/* 搜索区域 */}
  243. <Card>
  244. <CardHeader>
  245. <CardTitle>商户列表</CardTitle>
  246. <CardDescription>管理所有商户账户信息</CardDescription>
  247. </CardHeader>
  248. <CardContent>
  249. <form onSubmit={handleSearch} className="flex gap-2 mb-4">
  250. <div className="relative flex-1 max-w-sm">
  251. <Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
  252. <Input
  253. placeholder="搜索商户名称、用户名、手机号..."
  254. value={searchParams.search}
  255. onChange={(e) => setSearchParams(prev => ({ ...prev, search: e.target.value }))}
  256. className="pl-8"
  257. />
  258. </div>
  259. <Button type="submit" variant="outline" data-testid="search-button">
  260. 搜索
  261. </Button>
  262. </form>
  263. {/* 数据表格 */}
  264. {isLoading ? (
  265. renderSkeleton()
  266. ) : (
  267. <div className="rounded-md border">
  268. <Table>
  269. <TableHeader>
  270. <TableRow>
  271. <TableHead>商户名称</TableHead>
  272. <TableHead>用户名</TableHead>
  273. <TableHead>姓名</TableHead>
  274. <TableHead>手机号</TableHead>
  275. <TableHead>状态</TableHead>
  276. <TableHead>登录次数</TableHead>
  277. <TableHead>创建时间</TableHead>
  278. <TableHead className="text-right">操作</TableHead>
  279. </TableRow>
  280. </TableHeader>
  281. <TableBody>
  282. {data?.data.map((merchant) => (
  283. <TableRow key={merchant.id}>
  284. <TableCell>{merchant.name || '-'}</TableCell>
  285. <TableCell>{merchant.username}</TableCell>
  286. <TableCell>{merchant.realname || '-'}</TableCell>
  287. <TableCell>{merchant.phone || '-'}</TableCell>
  288. <TableCell>
  289. <Badge variant={getStateBadgeVariant(merchant.state)}>
  290. {getStateText(merchant.state)}
  291. </Badge>
  292. </TableCell>
  293. <TableCell>{merchant.loginNum}</TableCell>
  294. <TableCell>
  295. {format(new Date(merchant.createdAt), 'yyyy-MM-dd HH:mm', { locale: zhCN })}
  296. </TableCell>
  297. <TableCell className="text-right">
  298. <div className="flex justify-end gap-2">
  299. <Button
  300. variant="ghost"
  301. size="icon"
  302. onClick={() => handleViewDetail(merchant)}
  303. title="查看详情"
  304. data-testid={`view-detail-button-${merchant.id}`}
  305. >
  306. <Eye className="h-4 w-4" />
  307. </Button>
  308. <Button
  309. variant="ghost"
  310. size="icon"
  311. onClick={() => handleEditMerchant(merchant)}
  312. title="编辑"
  313. data-testid={`edit-button-${merchant.id}`}
  314. >
  315. <Edit className="h-4 w-4" />
  316. </Button>
  317. <Button
  318. variant="ghost"
  319. size="icon"
  320. onClick={() => handleDeleteMerchant(merchant.id)}
  321. title="删除"
  322. className="text-destructive hover:text-destructive"
  323. data-testid={`delete-button-${merchant.id}`}
  324. >
  325. <Trash2 className="h-4 w-4" />
  326. </Button>
  327. </div>
  328. </TableCell>
  329. </TableRow>
  330. ))}
  331. </TableBody>
  332. </Table>
  333. {data?.data.length === 0 && (
  334. <div className="text-center py-8">
  335. <p className="text-muted-foreground">暂无数据</p>
  336. </div>
  337. )}
  338. </div>
  339. )}
  340. {/* 分页组件 */}
  341. <DataTablePagination
  342. currentPage={searchParams.page}
  343. totalCount={data?.pagination.total || 0}
  344. pageSize={searchParams.limit}
  345. onPageChange={handlePageChange}
  346. />
  347. </CardContent>
  348. </Card>
  349. {/* 创建/编辑对话框 */}
  350. <Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
  351. <DialogContent className="sm:max-w-[600px] max-h-[90vh] overflow-y-auto">
  352. <DialogHeader>
  353. <DialogTitle>{isCreateForm ? '创建商户' : '编辑商户'}</DialogTitle>
  354. <DialogDescription>
  355. {isCreateForm ? '创建一个新的商户账户' : '编辑现有商户信息'}
  356. </DialogDescription>
  357. </DialogHeader>
  358. {isCreateForm ? (
  359. <Form {...createForm}>
  360. <form onSubmit={createForm.handleSubmit(handleSubmit)} className="space-y-4">
  361. <FormField
  362. control={createForm.control}
  363. name="name"
  364. render={({ field }) => (
  365. <FormItem>
  366. <FormLabel>商户名称</FormLabel>
  367. <FormControl>
  368. <Input placeholder="请输入商户名称" {...field} />
  369. </FormControl>
  370. <FormMessage />
  371. </FormItem>
  372. )}
  373. />
  374. <FormField
  375. control={createForm.control}
  376. name="username"
  377. render={({ field }) => (
  378. <FormItem>
  379. <FormLabel>用户名 <span className="text-red-500">*</span></FormLabel>
  380. <FormControl>
  381. <Input placeholder="请输入用户名" {...field} />
  382. </FormControl>
  383. <FormMessage />
  384. </FormItem>
  385. )}
  386. />
  387. <FormField
  388. control={createForm.control}
  389. name="password"
  390. render={({ field }) => (
  391. <FormItem>
  392. <FormLabel>密码 <span className="text-red-500">*</span></FormLabel>
  393. <FormControl>
  394. <Input type="password" placeholder="请输入密码" {...field} />
  395. </FormControl>
  396. <FormMessage />
  397. </FormItem>
  398. )}
  399. />
  400. <FormField
  401. control={createForm.control}
  402. name="phone"
  403. render={({ field }) => (
  404. <FormItem>
  405. <FormLabel>手机号</FormLabel>
  406. <FormControl>
  407. <Input placeholder="请输入手机号" {...field} />
  408. </FormControl>
  409. <FormMessage />
  410. </FormItem>
  411. )}
  412. />
  413. <FormField
  414. control={createForm.control}
  415. name="realname"
  416. render={({ field }) => (
  417. <FormItem>
  418. <FormLabel>姓名</FormLabel>
  419. <FormControl>
  420. <Input placeholder="请输入姓名" {...field} />
  421. </FormControl>
  422. <FormMessage />
  423. </FormItem>
  424. )}
  425. />
  426. <FormField
  427. control={createForm.control}
  428. name="state"
  429. render={({ field }) => (
  430. <FormItem>
  431. <FormLabel>状态</FormLabel>
  432. <Select onValueChange={(value) => field.onChange(parseInt(value))} defaultValue={field.value?.toString()}>
  433. <FormControl>
  434. <SelectTrigger>
  435. <SelectValue placeholder="请选择状态" />
  436. </SelectTrigger>
  437. </FormControl>
  438. <SelectContent>
  439. <SelectItem value="1">启用</SelectItem>
  440. <SelectItem value="2">禁用</SelectItem>
  441. </SelectContent>
  442. </Select>
  443. <FormMessage />
  444. </FormItem>
  445. )}
  446. />
  447. <DialogFooter>
  448. <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
  449. 取消
  450. </Button>
  451. <Button type="submit" disabled={createMutation.isPending} data-testid="create-submit-button">
  452. 创建
  453. </Button>
  454. </DialogFooter>
  455. </form>
  456. </Form>
  457. ) : (
  458. <Form {...updateForm}>
  459. <form onSubmit={updateForm.handleSubmit(handleSubmit)} className="space-y-4">
  460. <FormField
  461. control={updateForm.control}
  462. name="name"
  463. render={({ field }) => (
  464. <FormItem>
  465. <FormLabel>商户名称</FormLabel>
  466. <FormControl>
  467. <Input placeholder="请输入商户名称" {...field} />
  468. </FormControl>
  469. <FormMessage />
  470. </FormItem>
  471. )}
  472. />
  473. <FormField
  474. control={updateForm.control}
  475. name="username"
  476. render={({ field }) => (
  477. <FormItem>
  478. <FormLabel>用户名</FormLabel>
  479. <FormControl>
  480. <Input placeholder="请输入用户名" {...field} />
  481. </FormControl>
  482. <FormMessage />
  483. </FormItem>
  484. )}
  485. />
  486. <FormField
  487. control={updateForm.control}
  488. name="phone"
  489. render={({ field }) => (
  490. <FormItem>
  491. <FormLabel>手机号</FormLabel>
  492. <FormControl>
  493. <Input placeholder="请输入手机号" {...field} />
  494. </FormControl>
  495. <FormMessage />
  496. </FormItem>
  497. )}
  498. />
  499. <FormField
  500. control={updateForm.control}
  501. name="realname"
  502. render={({ field }) => (
  503. <FormItem>
  504. <FormLabel>姓名</FormLabel>
  505. <FormControl>
  506. <Input placeholder="请输入姓名" {...field} />
  507. </FormControl>
  508. <FormMessage />
  509. </FormItem>
  510. )}
  511. />
  512. <FormField
  513. control={updateForm.control}
  514. name="password"
  515. render={({ field }) => (
  516. <FormItem>
  517. <FormLabel>密码(留空则不修改)</FormLabel>
  518. <FormControl>
  519. <Input type="password" placeholder="请输入新密码" {...field} />
  520. </FormControl>
  521. <FormDescription>如果不修改密码,请留空</FormDescription>
  522. <FormMessage />
  523. </FormItem>
  524. )}
  525. />
  526. <FormField
  527. control={updateForm.control}
  528. name="state"
  529. render={({ field }) => (
  530. <FormItem>
  531. <FormLabel>状态</FormLabel>
  532. <Select onValueChange={(value) => field.onChange(parseInt(value))} value={field.value?.toString()}>
  533. <FormControl>
  534. <SelectTrigger>
  535. <SelectValue placeholder="请选择状态" />
  536. </SelectTrigger>
  537. </FormControl>
  538. <SelectContent>
  539. <SelectItem value="1">启用</SelectItem>
  540. <SelectItem value="2">禁用</SelectItem>
  541. </SelectContent>
  542. </Select>
  543. <FormMessage />
  544. </FormItem>
  545. )}
  546. />
  547. <DialogFooter>
  548. <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
  549. 取消
  550. </Button>
  551. <Button type="submit" disabled={updateMutation.isPending} data-testid="update-submit-button">
  552. 更新
  553. </Button>
  554. </DialogFooter>
  555. </form>
  556. </Form>
  557. )}
  558. </DialogContent>
  559. </Dialog>
  560. {/* 详情对话框 */}
  561. <Dialog open={detailDialogOpen} onOpenChange={setDetailDialogOpen}>
  562. <DialogContent className="sm:max-w-[500px]">
  563. <DialogHeader>
  564. <DialogTitle>商户详情</DialogTitle>
  565. <DialogDescription>查看商户详细信息</DialogDescription>
  566. </DialogHeader>
  567. {detailMerchant && (
  568. <div className="space-y-4">
  569. <div className="grid grid-cols-2 gap-4">
  570. <div>
  571. <label className="text-sm font-medium">商户名称</label>
  572. <p className="text-sm text-muted-foreground">{detailMerchant.name || '-'}</p>
  573. </div>
  574. <div>
  575. <label className="text-sm font-medium">用户名</label>
  576. <p className="text-sm text-muted-foreground">{detailMerchant.username}</p>
  577. </div>
  578. <div>
  579. <label className="text-sm font-medium">姓名</label>
  580. <p className="text-sm text-muted-foreground">{detailMerchant.realname || '-'}</p>
  581. </div>
  582. <div>
  583. <label className="text-sm font-medium">手机号</label>
  584. <p className="text-sm text-muted-foreground">{detailMerchant.phone || '-'}</p>
  585. </div>
  586. <div>
  587. <label className="text-sm font-medium">状态</label>
  588. <p className="text-sm">
  589. <Badge variant={getStateBadgeVariant(detailMerchant.state)}>
  590. {getStateText(detailMerchant.state)}
  591. </Badge>
  592. </p>
  593. </div>
  594. <div>
  595. <label className="text-sm font-medium">登录次数</label>
  596. <p className="text-sm text-muted-foreground">{detailMerchant.loginNum}</p>
  597. </div>
  598. <div>
  599. <label className="text-sm font-medium">创建时间</label>
  600. <p className="text-sm text-muted-foreground">
  601. {format(new Date(detailMerchant.createdAt), 'yyyy-MM-dd HH:mm', { locale: zhCN })}
  602. </p>
  603. </div>
  604. <div>
  605. <label className="text-sm font-medium">更新时间</label>
  606. <p className="text-sm text-muted-foreground">
  607. {format(new Date(detailMerchant.updatedAt), 'yyyy-MM-dd HH:mm', { locale: zhCN })}
  608. </p>
  609. </div>
  610. </div>
  611. {detailMerchant.lastLoginTime > 0 && (
  612. <div>
  613. <label className="text-sm font-medium">最后登录时间</label>
  614. <p className="text-sm text-muted-foreground">
  615. {format(new Date(detailMerchant.lastLoginTime * 1000), 'yyyy-MM-dd HH:mm', { locale: zhCN })}
  616. </p>
  617. </div>
  618. )}
  619. {detailMerchant.lastLoginIp && (
  620. <div>
  621. <label className="text-sm font-medium">最后登录IP</label>
  622. <p className="text-sm text-muted-foreground">{detailMerchant.lastLoginIp}</p>
  623. </div>
  624. )}
  625. </div>
  626. )}
  627. <DialogFooter>
  628. <Button onClick={() => setDetailDialogOpen(false)}>关闭</Button>
  629. </DialogFooter>
  630. </DialogContent>
  631. </Dialog>
  632. {/* 删除确认对话框 */}
  633. <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
  634. <DialogContent>
  635. <DialogHeader>
  636. <DialogTitle>确认删除</DialogTitle>
  637. <DialogDescription>
  638. 确定要删除这个商户吗?此操作无法撤销。
  639. </DialogDescription>
  640. </DialogHeader>
  641. <DialogFooter>
  642. <Button variant="outline" onClick={() => setDeleteDialogOpen(false)}>
  643. 取消
  644. </Button>
  645. <Button
  646. variant="destructive"
  647. onClick={confirmDelete}
  648. disabled={deleteMutation.isPending}
  649. >
  650. 删除
  651. </Button>
  652. </DialogFooter>
  653. </DialogContent>
  654. </Dialog>
  655. </div>
  656. )
  657. }