OrderManagement.tsx 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685
  1. import { useState } from 'react';
  2. import { useQuery } 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 { toast } from 'sonner';
  7. import { Search, Edit, Eye } from 'lucide-react';
  8. // 使用共享UI组件包的具体路径导入
  9. import { Button } from '@d8d/shared-ui-components/components/ui/button';
  10. import { Input } from '@d8d/shared-ui-components/components/ui/input';
  11. import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@d8d/shared-ui-components/components/ui/card';
  12. import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@d8d/shared-ui-components/components/ui/table';
  13. import { Badge } from '@d8d/shared-ui-components/components/ui/badge';
  14. import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@d8d/shared-ui-components/components/ui/dialog';
  15. import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@d8d/shared-ui-components/components/ui/form';
  16. import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@d8d/shared-ui-components/components/ui/select';
  17. import { Textarea } from '@d8d/shared-ui-components/components/ui/textarea';
  18. import { Skeleton } from '@d8d/shared-ui-components/components/ui/skeleton';
  19. // 简单分页组件
  20. const DataTablePagination = ({
  21. currentPage,
  22. pageSize,
  23. totalCount,
  24. onPageChange
  25. }: {
  26. currentPage: number;
  27. pageSize: number;
  28. totalCount: number;
  29. onPageChange: (page: number, limit: number) => void;
  30. }) => {
  31. const totalPages = Math.ceil(totalCount / pageSize);
  32. return (
  33. <div className="flex items-center justify-between px-2 py-4">
  34. <div className="text-sm text-muted-foreground">
  35. 共 {totalCount} 条记录
  36. </div>
  37. <div className="flex items-center space-x-2">
  38. <Button
  39. variant="outline"
  40. size="sm"
  41. onClick={() => onPageChange(Math.max(1, currentPage - 1), pageSize)}
  42. disabled={currentPage <= 1}
  43. >
  44. 上一页
  45. </Button>
  46. <div className="text-sm">
  47. 第 {currentPage} 页,共 {totalPages} 页
  48. </div>
  49. <Button
  50. variant="outline"
  51. size="sm"
  52. onClick={() => onPageChange(Math.min(totalPages, currentPage + 1), pageSize)}
  53. disabled={currentPage >= totalPages}
  54. >
  55. 下一页
  56. </Button>
  57. </div>
  58. </div>
  59. );
  60. };
  61. import { adminOrderClient } from '../api';
  62. import type { InferRequestType, InferResponseType } from 'hono/client';
  63. import { UpdateOrderDto } from '@d8d/orders-module/schemas';
  64. // 类型定义
  65. type OrderResponse = InferResponseType<typeof adminOrderClient.$get, 200>['data'][0];
  66. type UpdateRequest = InferRequestType<typeof adminOrderClient[':id']['$put']>['json'];
  67. // 状态映射
  68. const orderStatusMap = {
  69. 0: { label: '未发货', color: 'warning' },
  70. 1: { label: '已发货', color: 'info' },
  71. 2: { label: '收货成功', color: 'success' },
  72. 3: { label: '已退货', color: 'destructive' },
  73. } as const;
  74. const payStatusMap = {
  75. 0: { label: '未支付', color: 'warning' },
  76. 1: { label: '支付中', color: 'info' },
  77. 2: { label: '支付成功', color: 'success' },
  78. 3: { label: '已退款', color: 'secondary' },
  79. 4: { label: '支付失败', color: 'destructive' },
  80. 5: { label: '订单关闭', color: 'destructive' },
  81. } as const;
  82. const orderTypeMap = {
  83. 1: { label: '实物订单', color: 'default' },
  84. 2: { label: '虚拟订单', color: 'secondary' },
  85. } as const;
  86. export const OrderManagement = () => {
  87. const [searchParams, setSearchParams] = useState({
  88. page: 1,
  89. limit: 10,
  90. search: '',
  91. status: 'all',
  92. payStatus: 'all',
  93. });
  94. const [isModalOpen, setIsModalOpen] = useState(false);
  95. const [editingOrder, setEditingOrder] = useState<OrderResponse | null>(null);
  96. const [detailModalOpen, setDetailModalOpen] = useState(false);
  97. const [selectedOrder, setSelectedOrder] = useState<OrderResponse | null>(null);
  98. // 表单实例
  99. const updateForm = useForm<UpdateRequest>({
  100. resolver: zodResolver(UpdateOrderDto),
  101. defaultValues: {},
  102. });
  103. // 数据查询
  104. const { data, isLoading, refetch } = useQuery({
  105. queryKey: ['orders', searchParams],
  106. queryFn: async () => {
  107. const filters: any = {};
  108. if (searchParams.status !== 'all') {
  109. filters.state = parseInt(searchParams.status);
  110. }
  111. if (searchParams.payStatus !== 'all') {
  112. filters.payState = parseInt(searchParams.payStatus);
  113. }
  114. const res = await adminOrderClient.$get({
  115. query: {
  116. page: searchParams.page,
  117. pageSize: searchParams.limit,
  118. keyword: searchParams.search,
  119. ...(Object.keys(filters).length > 0 && { filters: JSON.stringify(filters) }),
  120. }
  121. });
  122. if (res.status !== 200) throw new Error('获取订单列表失败');
  123. return await res.json();
  124. }
  125. });
  126. // 处理搜索
  127. const handleSearch = () => {
  128. setSearchParams(prev => ({ ...prev, page: 1 }));
  129. };
  130. // 处理编辑订单
  131. const handleEditOrder = (order: OrderResponse) => {
  132. setEditingOrder(order);
  133. updateForm.reset({
  134. state: order.state,
  135. payState: order.payState,
  136. remark: order.remark || '',
  137. });
  138. setIsModalOpen(true);
  139. };
  140. // 处理查看详情
  141. const handleViewDetails = (order: OrderResponse) => {
  142. setSelectedOrder(order);
  143. setDetailModalOpen(true);
  144. };
  145. // 处理更新订单
  146. const handleUpdateSubmit = async (data: UpdateRequest) => {
  147. if (!editingOrder) return;
  148. try {
  149. const res = await adminOrderClient[':id']['$put']({
  150. param: { id: editingOrder.id.toString() },
  151. json: data,
  152. });
  153. if (res.status === 200) {
  154. toast.success('订单更新成功');
  155. setIsModalOpen(false);
  156. refetch();
  157. } else {
  158. const error = await res.json();
  159. toast.error(error.message || '更新失败');
  160. }
  161. } catch (error) {
  162. console.error('更新订单失败:', error);
  163. toast.error('更新失败,请重试');
  164. }
  165. };
  166. // 格式化金额
  167. const formatAmount = (amount: number) => {
  168. return `¥${amount.toFixed(2)}`;
  169. };
  170. // 获取状态颜色
  171. const getStatusBadge = (status: number, type: 'order' | 'pay') => {
  172. const map = type === 'order' ? orderStatusMap : payStatusMap;
  173. const config = map[status as keyof typeof map] || { label: '未知', color: 'default' };
  174. return <Badge variant={config.color as any}>{config.label}</Badge>;
  175. };
  176. // 骨架屏 - 只覆盖表格区域,搜索区域保持可用
  177. if (isLoading) {
  178. return (
  179. <div className="space-y-4">
  180. {/* 页面标题 */}
  181. <div className="flex justify-between items-center">
  182. <div>
  183. <h1 className="text-2xl font-bold">订单管理</h1>
  184. <p className="text-muted-foreground">管理所有订单信息</p>
  185. </div>
  186. </div>
  187. {/* 搜索区域 - 保持可用 */}
  188. <Card>
  189. <CardHeader>
  190. <CardTitle>订单列表</CardTitle>
  191. <CardDescription>查看和管理所有订单</CardDescription>
  192. </CardHeader>
  193. <CardContent>
  194. <div className="flex gap-4 mb-4">
  195. <div className="relative flex-1 max-w-sm">
  196. <Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
  197. <Input
  198. placeholder="搜索订单号、手机号、收货人姓名..."
  199. value={searchParams.search}
  200. onChange={(e) => setSearchParams(prev => ({ ...prev, search: e.target.value }))}
  201. className="pl-8"
  202. data-testid="order-search-input"
  203. />
  204. </div>
  205. <Select
  206. value={searchParams.status}
  207. onValueChange={(value) => setSearchParams(prev => ({ ...prev, status: value, page: 1 }))}
  208. >
  209. <SelectTrigger className="w-32" data-testid="order-status-select">
  210. <SelectValue placeholder="订单状态" />
  211. </SelectTrigger>
  212. <SelectContent>
  213. <SelectItem value="all">全部</SelectItem>
  214. <SelectItem value="0">未发货</SelectItem>
  215. <SelectItem value="1">已发货</SelectItem>
  216. <SelectItem value="2">收货成功</SelectItem>
  217. <SelectItem value="3">已退货</SelectItem>
  218. </SelectContent>
  219. </Select>
  220. <Select
  221. value={searchParams.payStatus}
  222. onValueChange={(value) => setSearchParams(prev => ({ ...prev, payStatus: value, page: 1 }))}
  223. >
  224. <SelectTrigger className="w-32" data-testid="order-pay-status-select">
  225. <SelectValue placeholder="支付状态" />
  226. </SelectTrigger>
  227. <SelectContent>
  228. <SelectItem value="all">全部</SelectItem>
  229. <SelectItem value="0">未支付</SelectItem>
  230. <SelectItem value="1">支付中</SelectItem>
  231. <SelectItem value="2">支付成功</SelectItem>
  232. <SelectItem value="3">已退款</SelectItem>
  233. <SelectItem value="4">支付失败</SelectItem>
  234. <SelectItem value="5">订单关闭</SelectItem>
  235. </SelectContent>
  236. </Select>
  237. <Button onClick={handleSearch} data-testid="order-search-button">
  238. <Search className="h-4 w-4 mr-2" />
  239. 搜索
  240. </Button>
  241. </div>
  242. {/* 表格骨架屏 */}
  243. <div className="rounded-md border">
  244. <Table>
  245. <TableHeader>
  246. <TableRow>
  247. <TableHead>订单号</TableHead>
  248. <TableHead>用户信息</TableHead>
  249. <TableHead>收货人</TableHead>
  250. <TableHead>金额</TableHead>
  251. <TableHead>订单状态</TableHead>
  252. <TableHead>支付状态</TableHead>
  253. <TableHead>创建时间</TableHead>
  254. <TableHead className="text-right">操作</TableHead>
  255. </TableRow>
  256. </TableHeader>
  257. <TableBody>
  258. {[...Array(5)].map((_, i) => (
  259. <TableRow key={i}>
  260. <TableCell>
  261. <Skeleton className="h-4 w-32" />
  262. </TableCell>
  263. <TableCell>
  264. <Skeleton className="h-4 w-24" />
  265. </TableCell>
  266. <TableCell>
  267. <Skeleton className="h-4 w-20" />
  268. </TableCell>
  269. <TableCell>
  270. <Skeleton className="h-4 w-16" />
  271. </TableCell>
  272. <TableCell>
  273. <Skeleton className="h-6 w-16" />
  274. </TableCell>
  275. <TableCell>
  276. <Skeleton className="h-6 w-16" />
  277. </TableCell>
  278. <TableCell>
  279. <Skeleton className="h-4 w-24" />
  280. </TableCell>
  281. <TableCell className="text-right">
  282. <div className="flex justify-end gap-2">
  283. <Skeleton className="h-8 w-8" />
  284. <Skeleton className="h-8 w-8" />
  285. </div>
  286. </TableCell>
  287. </TableRow>
  288. ))}
  289. </TableBody>
  290. </Table>
  291. </div>
  292. {/* 分页骨架屏 */}
  293. <div className="flex items-center justify-between px-2 py-4">
  294. <Skeleton className="h-4 w-32" />
  295. <div className="flex items-center space-x-2">
  296. <Skeleton className="h-8 w-16" />
  297. <Skeleton className="h-4 w-24" />
  298. <Skeleton className="h-8 w-16" />
  299. </div>
  300. </div>
  301. </CardContent>
  302. </Card>
  303. </div>
  304. );
  305. }
  306. return (
  307. <div className="space-y-4">
  308. {/* 页面标题 */}
  309. <div className="flex justify-between items-center">
  310. <div>
  311. <h1 className="text-2xl font-bold">订单管理</h1>
  312. <p className="text-muted-foreground">管理所有订单信息</p>
  313. </div>
  314. </div>
  315. {/* 搜索区域 */}
  316. <Card>
  317. <CardHeader>
  318. <CardTitle>订单列表</CardTitle>
  319. <CardDescription>查看和管理所有订单</CardDescription>
  320. </CardHeader>
  321. <CardContent>
  322. <div className="flex gap-4 mb-4">
  323. <div className="relative flex-1 max-w-sm">
  324. <Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
  325. <Input
  326. placeholder="搜索订单号、手机号、收货人姓名..."
  327. value={searchParams.search}
  328. onChange={(e) => setSearchParams(prev => ({ ...prev, search: e.target.value }))}
  329. className="pl-8"
  330. data-testid="order-search-input"
  331. />
  332. </div>
  333. <Select
  334. value={searchParams.status}
  335. onValueChange={(value) => setSearchParams(prev => ({ ...prev, status: value, page: 1 }))}
  336. >
  337. <SelectTrigger className="w-32" data-testid="order-status-select">
  338. <SelectValue placeholder="订单状态" />
  339. </SelectTrigger>
  340. <SelectContent>
  341. <SelectItem value="all">全部</SelectItem>
  342. <SelectItem value="0">未发货</SelectItem>
  343. <SelectItem value="1">已发货</SelectItem>
  344. <SelectItem value="2">收货成功</SelectItem>
  345. <SelectItem value="3">已退货</SelectItem>
  346. </SelectContent>
  347. </Select>
  348. <Select
  349. value={searchParams.payStatus}
  350. onValueChange={(value) => setSearchParams(prev => ({ ...prev, payStatus: value, page: 1 }))}
  351. >
  352. <SelectTrigger className="w-32" data-testid="order-pay-status-select">
  353. <SelectValue placeholder="支付状态" />
  354. </SelectTrigger>
  355. <SelectContent>
  356. <SelectItem value="all">全部</SelectItem>
  357. <SelectItem value="0">未支付</SelectItem>
  358. <SelectItem value="1">支付中</SelectItem>
  359. <SelectItem value="2">支付成功</SelectItem>
  360. <SelectItem value="3">已退款</SelectItem>
  361. <SelectItem value="4">支付失败</SelectItem>
  362. <SelectItem value="5">订单关闭</SelectItem>
  363. </SelectContent>
  364. </Select>
  365. <Button onClick={handleSearch} data-testid="order-search-button">
  366. <Search className="h-4 w-4 mr-2" />
  367. 搜索
  368. </Button>
  369. </div>
  370. {/* 数据表格 */}
  371. <div className="rounded-md border">
  372. <Table>
  373. <TableHeader>
  374. <TableRow>
  375. <TableHead>订单号</TableHead>
  376. <TableHead>用户信息</TableHead>
  377. <TableHead>收货人</TableHead>
  378. <TableHead>金额</TableHead>
  379. <TableHead>订单状态</TableHead>
  380. <TableHead>支付状态</TableHead>
  381. <TableHead>创建时间</TableHead>
  382. <TableHead className="text-right">操作</TableHead>
  383. </TableRow>
  384. </TableHeader>
  385. <TableBody>
  386. {data?.data.map((order) => (
  387. <TableRow key={order.id}>
  388. <TableCell>
  389. <div>
  390. <p className="font-medium">{order.orderNo}</p>
  391. <p className="text-sm text-muted-foreground">
  392. {orderTypeMap[order.orderType as keyof typeof orderTypeMap]?.label}
  393. </p>
  394. </div>
  395. </TableCell>
  396. <TableCell>
  397. <div>
  398. <p>{order.user?.username || '-'}</p>
  399. <p className="text-sm text-muted-foreground">{order.userPhone}</p>
  400. </div>
  401. </TableCell>
  402. <TableCell>
  403. <div>
  404. <p>{order.recevierName || '-'}</p>
  405. <p className="text-sm text-muted-foreground">{order.receiverMobile}</p>
  406. </div>
  407. </TableCell>
  408. <TableCell>
  409. <div>
  410. <p className="font-medium">{formatAmount(order.payAmount)}</p>
  411. <p className="text-sm text-muted-foreground">{formatAmount(order.amount)}</p>
  412. </div>
  413. </TableCell>
  414. <TableCell>{getStatusBadge(order.state, 'order')}</TableCell>
  415. <TableCell>{getStatusBadge(order.payState, 'pay')}</TableCell>
  416. <TableCell>
  417. {format(new Date(order.createdAt), 'yyyy-MM-dd HH:mm')}
  418. </TableCell>
  419. <TableCell className="text-right">
  420. <div className="flex justify-end gap-2">
  421. <Button
  422. variant="ghost"
  423. size="icon"
  424. onClick={() => handleViewDetails(order)}
  425. data-testid="order-view-button"
  426. >
  427. <Eye className="h-4 w-4" />
  428. </Button>
  429. <Button
  430. variant="ghost"
  431. size="icon"
  432. onClick={() => handleEditOrder(order)}
  433. data-testid="order-edit-button"
  434. >
  435. <Edit className="h-4 w-4" />
  436. </Button>
  437. </div>
  438. </TableCell>
  439. </TableRow>
  440. ))}
  441. </TableBody>
  442. </Table>
  443. </div>
  444. {data?.data.length === 0 && !isLoading && (
  445. <div className="text-center py-8">
  446. <p className="text-muted-foreground">暂无订单数据</p>
  447. </div>
  448. )}
  449. <DataTablePagination
  450. currentPage={searchParams.page}
  451. pageSize={searchParams.limit}
  452. totalCount={data?.pagination.total || 0}
  453. onPageChange={(page, limit) => setSearchParams(prev => ({ ...prev, page, limit }))}
  454. />
  455. </CardContent>
  456. </Card>
  457. {/* 编辑订单模态框 */}
  458. <Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
  459. <DialogContent className="sm:max-w-[500px] max-h-[90vh] overflow-y-auto">
  460. <DialogHeader>
  461. <DialogTitle>编辑订单</DialogTitle>
  462. <DialogDescription>更新订单状态和备注信息</DialogDescription>
  463. </DialogHeader>
  464. <Form {...updateForm}>
  465. <form onSubmit={updateForm.handleSubmit(handleUpdateSubmit)} className="space-y-4">
  466. <FormField
  467. control={updateForm.control}
  468. name="state"
  469. render={({ field }) => (
  470. <FormItem>
  471. <FormLabel>订单状态</FormLabel>
  472. <Select onValueChange={(value) => field.onChange(parseInt(value))} value={field.value?.toString()}>
  473. <FormControl>
  474. <SelectTrigger data-testid="edit-order-status-select">
  475. <SelectValue placeholder="选择订单状态" />
  476. </SelectTrigger>
  477. </FormControl>
  478. <SelectContent>
  479. <SelectItem value="0">未发货</SelectItem>
  480. <SelectItem value="1">已发货</SelectItem>
  481. <SelectItem value="2">收货成功</SelectItem>
  482. <SelectItem value="3">已退货</SelectItem>
  483. </SelectContent>
  484. </Select>
  485. <FormMessage />
  486. </FormItem>
  487. )}
  488. />
  489. <FormField
  490. control={updateForm.control}
  491. name="payState"
  492. render={({ field }) => (
  493. <FormItem>
  494. <FormLabel>支付状态</FormLabel>
  495. <Select onValueChange={(value) => field.onChange(parseInt(value))} value={field.value?.toString()}>
  496. <FormControl>
  497. <SelectTrigger data-testid="edit-pay-status-select">
  498. <SelectValue placeholder="选择支付状态" />
  499. </SelectTrigger>
  500. </FormControl>
  501. <SelectContent>
  502. <SelectItem value="0">未支付</SelectItem>
  503. <SelectItem value="1">支付中</SelectItem>
  504. <SelectItem value="2">支付成功</SelectItem>
  505. <SelectItem value="3">已退款</SelectItem>
  506. <SelectItem value="4">支付失败</SelectItem>
  507. <SelectItem value="5">订单关闭</SelectItem>
  508. </SelectContent>
  509. </Select>
  510. <FormMessage />
  511. </FormItem>
  512. )}
  513. />
  514. <FormField
  515. control={updateForm.control}
  516. name="remark"
  517. render={({ field }) => (
  518. <FormItem>
  519. <FormLabel>管理员备注</FormLabel>
  520. <FormControl>
  521. <Textarea
  522. placeholder="输入管理员备注信息..."
  523. className="resize-none"
  524. data-testid="edit-remark-textarea"
  525. {...field}
  526. />
  527. </FormControl>
  528. <FormMessage />
  529. </FormItem>
  530. )}
  531. />
  532. <DialogFooter>
  533. <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
  534. 取消
  535. </Button>
  536. <Button type="submit" data-testid="order-save-button">保存</Button>
  537. </DialogFooter>
  538. </form>
  539. </Form>
  540. </DialogContent>
  541. </Dialog>
  542. {/* 订单详情模态框 */}
  543. <Dialog open={detailModalOpen} onOpenChange={setDetailModalOpen}>
  544. <DialogContent className="sm:max-w-[700px] max-h-[90vh] overflow-y-auto">
  545. <DialogHeader>
  546. <DialogTitle>订单详情</DialogTitle>
  547. <DialogDescription>查看订单的详细信息</DialogDescription>
  548. </DialogHeader>
  549. {selectedOrder && (
  550. <div className="space-y-4">
  551. <div className="grid grid-cols-2 gap-4">
  552. <div>
  553. <h4 className="font-medium mb-2">订单信息</h4>
  554. <div className="space-y-2 text-sm">
  555. <div className="flex justify-between">
  556. <span className="text-muted-foreground">订单号:</span>
  557. <span>{selectedOrder.orderNo}</span>
  558. </div>
  559. <div className="flex justify-between">
  560. <span className="text-muted-foreground">订单类型:</span>
  561. <span>{orderTypeMap[selectedOrder.orderType as keyof typeof orderTypeMap]?.label}</span>
  562. </div>
  563. <div className="flex justify-between">
  564. <span className="text-muted-foreground">订单金额:</span>
  565. <span>{formatAmount(selectedOrder.amount)}</span>
  566. </div>
  567. <div className="flex justify-between">
  568. <span className="text-muted-foreground">实付金额:</span>
  569. <span>{formatAmount(selectedOrder.payAmount)}</span>
  570. </div>
  571. <div className="flex justify-between">
  572. <span className="text-muted-foreground">运费:</span>
  573. <span>{formatAmount(selectedOrder.freightAmount)}</span>
  574. </div>
  575. </div>
  576. </div>
  577. <div>
  578. <h4 className="font-medium mb-2">状态信息</h4>
  579. <div className="space-y-2 text-sm">
  580. <div className="flex justify-between">
  581. <span className="text-muted-foreground">订单状态:</span>
  582. <span>{getStatusBadge(selectedOrder.state, 'order')}</span>
  583. </div>
  584. <div className="flex justify-between">
  585. <span className="text-muted-foreground">支付状态:</span>
  586. <span>{getStatusBadge(selectedOrder.payState, 'pay')}</span>
  587. </div>
  588. <div className="flex justify-between">
  589. <span className="text-muted-foreground">支付方式:</span>
  590. <span>{selectedOrder.payType === 1 ? '积分' : selectedOrder.payType === 2 ? '礼券' : '未选择'}</span>
  591. </div>
  592. <div className="flex justify-between">
  593. <span className="text-muted-foreground">创建时间:</span>
  594. <span>{format(new Date(selectedOrder.createdAt), 'yyyy-MM-dd HH:mm')}</span>
  595. </div>
  596. </div>
  597. </div>
  598. </div>
  599. <div className="grid grid-cols-2 gap-4">
  600. <div>
  601. <h4 className="font-medium mb-2">用户信息</h4>
  602. <div className="space-y-2 text-sm">
  603. <div className="flex justify-between">
  604. <span className="text-muted-foreground">用户名:</span>
  605. <span>{selectedOrder.user?.username || '-'}</span>
  606. </div>
  607. <div className="flex justify-between">
  608. <span className="text-muted-foreground">手机号:</span>
  609. <span>{selectedOrder.userPhone || '-'}</span>
  610. </div>
  611. </div>
  612. </div>
  613. <div>
  614. <h4 className="font-medium mb-2">收货信息</h4>
  615. <div className="space-y-2 text-sm">
  616. <div className="flex justify-between">
  617. <span className="text-muted-foreground">收货人:</span>
  618. <span>{selectedOrder.recevierName || '-'}</span>
  619. </div>
  620. <div className="flex justify-between">
  621. <span className="text-muted-foreground">手机号:</span>
  622. <span>{selectedOrder.receiverMobile || '-'}</span>
  623. </div>
  624. <div className="flex justify-between">
  625. <span className="text-muted-foreground">地址:</span>
  626. <span>{selectedOrder.address || '-'}</span>
  627. </div>
  628. </div>
  629. </div>
  630. </div>
  631. {selectedOrder.remark && (
  632. <div>
  633. <h4 className="font-medium mb-2">管理员备注</h4>
  634. <p className="text-sm bg-muted p-3 rounded-md">{selectedOrder.remark}</p>
  635. </div>
  636. )}
  637. </div>
  638. )}
  639. </DialogContent>
  640. </Dialog>
  641. </div>
  642. );
  643. };