OrderManagement.tsx 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939
  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, Package, Truck } 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, orderClientManager } from '../api';
  62. import type { InferResponseType } from 'hono/client';
  63. import { UpdateOrderDto } from '@d8d/orders-module-mt/schemas';
  64. // 类型定义
  65. type OrderResponse = InferResponseType<typeof adminOrderClient.index.$get, 200>['data'][0];
  66. type UpdateRequest = any;
  67. type DeliveryRequest = any;
  68. type DeliveryResponse = any;
  69. // 状态映射
  70. const orderStatusMap = {
  71. 0: { label: '未发货', color: 'warning' },
  72. 1: { label: '已发货', color: 'info' },
  73. 2: { label: '收货成功', color: 'success' },
  74. 3: { label: '已退货', color: 'destructive' },
  75. } as const;
  76. const payStatusMap = {
  77. 0: { label: '未支付', color: 'warning' },
  78. 1: { label: '支付中', color: 'info' },
  79. 2: { label: '支付成功', color: 'success' },
  80. 3: { label: '已退款', color: 'secondary' },
  81. 4: { label: '支付失败', color: 'destructive' },
  82. 5: { label: '订单关闭', color: 'destructive' },
  83. } as const;
  84. const orderTypeMap = {
  85. 1: { label: '实物订单', color: 'default' },
  86. 2: { label: '虚拟订单', color: 'secondary' },
  87. } as const;
  88. const deliveryTypeMap = {
  89. 0: { label: '未发货', color: 'warning' },
  90. 1: { label: '物流快递', color: 'info' },
  91. 2: { label: '同城配送', color: 'info' },
  92. 3: { label: '用户自提', color: 'info' },
  93. 4: { label: '虚拟发货', color: 'info' },
  94. } as const;
  95. export const OrderManagement = () => {
  96. const [searchParams, setSearchParams] = useState({
  97. page: 1,
  98. limit: 10,
  99. search: '',
  100. status: 'all',
  101. payStatus: 'all',
  102. });
  103. const [isModalOpen, setIsModalOpen] = useState(false);
  104. const [editingOrder, setEditingOrder] = useState<OrderResponse | null>(null);
  105. const [detailModalOpen, setDetailModalOpen] = useState(false);
  106. const [selectedOrder, setSelectedOrder] = useState<OrderResponse | null>(null);
  107. const [deliveryModalOpen, setDeliveryModalOpen] = useState(false);
  108. const [deliveringOrder, setDeliveringOrder] = useState<OrderResponse | null>(null);
  109. // 表单实例
  110. const updateForm = useForm<UpdateRequest>({
  111. resolver: zodResolver(UpdateOrderDto),
  112. defaultValues: {},
  113. });
  114. // 发货表单实例
  115. const deliveryForm = useForm<DeliveryRequest>({
  116. defaultValues: {
  117. deliveryType: 1,
  118. deliveryCompany: '',
  119. deliveryNo: '',
  120. deliveryRemark: '',
  121. },
  122. });
  123. // 数据查询 - 60秒自动刷新
  124. const { data, isLoading, refetch } = useQuery({
  125. queryKey: ['orders', searchParams],
  126. queryFn: async () => {
  127. const filters: any = {};
  128. if (searchParams.status !== 'all') {
  129. filters.state = parseInt(searchParams.status);
  130. }
  131. if (searchParams.payStatus !== 'all') {
  132. filters.payState = parseInt(searchParams.payStatus);
  133. }
  134. const res = await orderClientManager.getAdminOrderClient().index.$get({
  135. query: {
  136. page: searchParams.page,
  137. pageSize: searchParams.limit,
  138. keyword: searchParams.search,
  139. ...(Object.keys(filters).length > 0 && { filters: JSON.stringify(filters) }),
  140. }
  141. });
  142. if (res.status !== 200) throw new Error('获取订单列表失败');
  143. return await res.json();
  144. },
  145. refetchInterval: 60000, // 60秒自动刷新
  146. refetchIntervalInBackground: false, // 只在页面可见时刷新
  147. staleTime: 30000, // 30秒后数据视为过期
  148. });
  149. // 处理搜索
  150. const handleSearch = () => {
  151. setSearchParams(prev => ({ ...prev, page: 1 }));
  152. };
  153. // 处理编辑订单
  154. const handleEditOrder = (order: OrderResponse) => {
  155. setEditingOrder(order);
  156. updateForm.reset({
  157. state: order.state,
  158. payState: order.payState,
  159. remark: order.remark || '',
  160. });
  161. setIsModalOpen(true);
  162. };
  163. // 处理查看详情
  164. const handleViewDetails = (order: OrderResponse) => {
  165. setSelectedOrder(order);
  166. setDetailModalOpen(true);
  167. };
  168. // 处理发货
  169. const handleDeliveryOrder = (order: OrderResponse) => {
  170. setDeliveringOrder(order);
  171. deliveryForm.reset({
  172. deliveryType: 1,
  173. deliveryCompany: '',
  174. deliveryNo: '',
  175. deliveryRemark: '',
  176. });
  177. setDeliveryModalOpen(true);
  178. };
  179. // 处理发货提交
  180. const handleDeliverySubmit = async (data: DeliveryRequest) => {
  181. if (!deliveringOrder || !deliveringOrder.id) return;
  182. try {
  183. const res = await (orderClientManager.getAdminDeliveryClient() as any)[':id']['delivery']['$post']({
  184. param: { id: deliveringOrder.id },
  185. json: data,
  186. });
  187. if (res.status === 200) {
  188. const result = await res.json() as DeliveryResponse;
  189. toast.success(result.message || '发货成功');
  190. setDeliveryModalOpen(false);
  191. refetch();
  192. } else {
  193. const error = await res.json();
  194. toast.error(error.message || '发货失败');
  195. }
  196. } catch (error) {
  197. console.error('发货失败:', error);
  198. toast.error('发货失败,请重试');
  199. }
  200. };
  201. // 处理更新订单
  202. const handleUpdateSubmit = async (data: UpdateRequest) => {
  203. if (!editingOrder || !editingOrder.id) return;
  204. try {
  205. const res = await (orderClientManager.getAdminOrderClient() as any)[':id']['$put']({
  206. param: { id: editingOrder.id },
  207. json: data,
  208. });
  209. if (res.status === 200) {
  210. toast.success('订单更新成功');
  211. setIsModalOpen(false);
  212. refetch();
  213. } else {
  214. const error = await res.json();
  215. toast.error(error.message || '更新失败');
  216. }
  217. } catch (error) {
  218. console.error('更新订单失败:', error);
  219. toast.error('更新失败,请重试');
  220. }
  221. };
  222. // 格式化金额
  223. const formatAmount = (amount: number) => {
  224. return `¥${amount.toFixed(2)}`;
  225. };
  226. // 获取状态颜色
  227. const getStatusBadge = (status: number, type: 'order' | 'pay' | 'delivery') => {
  228. const map = type === 'order' ? orderStatusMap : type === 'pay' ? payStatusMap : deliveryTypeMap;
  229. const config = map[status as keyof typeof map] || { label: '未知', color: 'default' };
  230. return <Badge variant={config.color as any}>{config.label}</Badge>;
  231. };
  232. // 骨架屏 - 只覆盖表格区域,搜索区域保持可用
  233. if (isLoading) {
  234. return (
  235. <div className="space-y-4">
  236. {/* 页面标题 */}
  237. <div className="flex justify-between items-center">
  238. <div>
  239. <h1 className="text-2xl font-bold">订单管理</h1>
  240. <p className="text-muted-foreground">管理所有订单信息</p>
  241. </div>
  242. </div>
  243. {/* 搜索区域 - 保持可用 */}
  244. <Card>
  245. <CardHeader>
  246. <CardTitle>订单列表</CardTitle>
  247. <CardDescription>查看和管理所有订单</CardDescription>
  248. </CardHeader>
  249. <CardContent>
  250. <div className="flex gap-4 mb-4">
  251. <div className="relative flex-1 max-w-sm">
  252. <Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
  253. <Input
  254. placeholder="搜索订单号、手机号、收货人姓名..."
  255. value={searchParams.search}
  256. onChange={(e) => setSearchParams(prev => ({ ...prev, search: e.target.value }))}
  257. className="pl-8"
  258. data-testid="order-search-input"
  259. />
  260. </div>
  261. <Select
  262. value={searchParams.status}
  263. onValueChange={(value) => setSearchParams(prev => ({ ...prev, status: value, page: 1 }))}
  264. >
  265. <SelectTrigger className="w-32" data-testid="order-status-select">
  266. <SelectValue placeholder="订单状态" />
  267. </SelectTrigger>
  268. <SelectContent>
  269. <SelectItem value="all">全部</SelectItem>
  270. <SelectItem value="0">未发货</SelectItem>
  271. <SelectItem value="1">已发货</SelectItem>
  272. <SelectItem value="2">收货成功</SelectItem>
  273. <SelectItem value="3">已退货</SelectItem>
  274. </SelectContent>
  275. </Select>
  276. <Select
  277. value={searchParams.payStatus}
  278. onValueChange={(value) => setSearchParams(prev => ({ ...prev, payStatus: value, page: 1 }))}
  279. >
  280. <SelectTrigger className="w-32" data-testid="order-pay-status-select">
  281. <SelectValue placeholder="支付状态" />
  282. </SelectTrigger>
  283. <SelectContent>
  284. <SelectItem value="all">全部</SelectItem>
  285. <SelectItem value="0">未支付</SelectItem>
  286. <SelectItem value="1">支付中</SelectItem>
  287. <SelectItem value="2">支付成功</SelectItem>
  288. <SelectItem value="3">已退款</SelectItem>
  289. <SelectItem value="4">支付失败</SelectItem>
  290. <SelectItem value="5">订单关闭</SelectItem>
  291. </SelectContent>
  292. </Select>
  293. <Button onClick={handleSearch} data-testid="order-search-button">
  294. <Search className="h-4 w-4 mr-2" />
  295. 搜索
  296. </Button>
  297. </div>
  298. {/* 表格骨架屏 */}
  299. <div className="rounded-md border">
  300. <Table>
  301. <TableHeader>
  302. <TableRow>
  303. <TableHead>订单号</TableHead>
  304. <TableHead>用户信息</TableHead>
  305. <TableHead>收货人</TableHead>
  306. <TableHead>金额</TableHead>
  307. <TableHead>订单状态</TableHead>
  308. <TableHead>支付状态</TableHead>
  309. <TableHead>创建时间</TableHead>
  310. <TableHead className="text-right">操作</TableHead>
  311. </TableRow>
  312. </TableHeader>
  313. <TableBody>
  314. {[...Array(5)].map((_, i) => (
  315. <TableRow key={i}>
  316. <TableCell>
  317. <Skeleton className="h-4 w-32" />
  318. </TableCell>
  319. <TableCell>
  320. <Skeleton className="h-4 w-24" />
  321. </TableCell>
  322. <TableCell>
  323. <Skeleton className="h-4 w-20" />
  324. </TableCell>
  325. <TableCell>
  326. <Skeleton className="h-4 w-16" />
  327. </TableCell>
  328. <TableCell>
  329. <Skeleton className="h-6 w-16" />
  330. </TableCell>
  331. <TableCell>
  332. <Skeleton className="h-6 w-16" />
  333. </TableCell>
  334. <TableCell>
  335. <Skeleton className="h-4 w-24" />
  336. </TableCell>
  337. <TableCell className="text-right">
  338. <div className="flex justify-end gap-2">
  339. <Skeleton className="h-8 w-8" />
  340. <Skeleton className="h-8 w-8" />
  341. </div>
  342. </TableCell>
  343. </TableRow>
  344. ))}
  345. </TableBody>
  346. </Table>
  347. </div>
  348. {/* 分页骨架屏 */}
  349. <div className="flex items-center justify-between px-2 py-4">
  350. <Skeleton className="h-4 w-32" />
  351. <div className="flex items-center space-x-2">
  352. <Skeleton className="h-8 w-16" />
  353. <Skeleton className="h-4 w-24" />
  354. <Skeleton className="h-8 w-16" />
  355. </div>
  356. </div>
  357. </CardContent>
  358. </Card>
  359. </div>
  360. );
  361. }
  362. return (
  363. <div className="space-y-4">
  364. {/* 页面标题 */}
  365. <div className="flex justify-between items-center">
  366. <div>
  367. <h1 className="text-2xl font-bold">订单管理</h1>
  368. <p className="text-muted-foreground">管理所有订单信息</p>
  369. </div>
  370. </div>
  371. {/* 搜索区域 */}
  372. <Card>
  373. <CardHeader>
  374. <CardTitle>订单列表</CardTitle>
  375. <CardDescription>查看和管理所有订单</CardDescription>
  376. </CardHeader>
  377. <CardContent>
  378. <div className="flex gap-4 mb-4">
  379. <div className="relative flex-1 max-w-sm">
  380. <Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
  381. <Input
  382. placeholder="搜索订单号、手机号、收货人姓名..."
  383. value={searchParams.search}
  384. onChange={(e) => setSearchParams(prev => ({ ...prev, search: e.target.value }))}
  385. className="pl-8"
  386. data-testid="order-search-input"
  387. />
  388. </div>
  389. <Select
  390. value={searchParams.status}
  391. onValueChange={(value) => setSearchParams(prev => ({ ...prev, status: value, page: 1 }))}
  392. >
  393. <SelectTrigger className="w-32" data-testid="order-status-select">
  394. <SelectValue placeholder="订单状态" />
  395. </SelectTrigger>
  396. <SelectContent>
  397. <SelectItem value="all">全部</SelectItem>
  398. <SelectItem value="0">未发货</SelectItem>
  399. <SelectItem value="1">已发货</SelectItem>
  400. <SelectItem value="2">收货成功</SelectItem>
  401. <SelectItem value="3">已退货</SelectItem>
  402. </SelectContent>
  403. </Select>
  404. <Select
  405. value={searchParams.payStatus}
  406. onValueChange={(value) => setSearchParams(prev => ({ ...prev, payStatus: value, page: 1 }))}
  407. >
  408. <SelectTrigger className="w-32" data-testid="order-pay-status-select">
  409. <SelectValue placeholder="支付状态" />
  410. </SelectTrigger>
  411. <SelectContent>
  412. <SelectItem value="all">全部</SelectItem>
  413. <SelectItem value="0">未支付</SelectItem>
  414. <SelectItem value="1">支付中</SelectItem>
  415. <SelectItem value="2">支付成功</SelectItem>
  416. <SelectItem value="3">已退款</SelectItem>
  417. <SelectItem value="4">支付失败</SelectItem>
  418. <SelectItem value="5">订单关闭</SelectItem>
  419. </SelectContent>
  420. </Select>
  421. <Button onClick={handleSearch} data-testid="order-search-button">
  422. <Search className="h-4 w-4 mr-2" />
  423. 搜索
  424. </Button>
  425. </div>
  426. {/* 数据表格 */}
  427. <div className="rounded-md border">
  428. <Table>
  429. <TableHeader>
  430. <TableRow>
  431. <TableHead>订单号</TableHead>
  432. <TableHead>用户信息</TableHead>
  433. <TableHead>收货人</TableHead>
  434. <TableHead>金额</TableHead>
  435. <TableHead>订单状态</TableHead>
  436. <TableHead>支付状态</TableHead>
  437. <TableHead>创建时间</TableHead>
  438. <TableHead className="text-right">操作</TableHead>
  439. </TableRow>
  440. </TableHeader>
  441. <TableBody>
  442. {data?.data.map((order) => (
  443. <TableRow key={order.id}>
  444. <TableCell>
  445. <div>
  446. <p className="font-medium">{order.orderNo}</p>
  447. <p className="text-sm text-muted-foreground">
  448. {orderTypeMap[order.orderType as keyof typeof orderTypeMap]?.label}
  449. </p>
  450. </div>
  451. </TableCell>
  452. <TableCell>
  453. <div>
  454. <p>{order.user?.username || '-'}</p>
  455. <p className="text-sm text-muted-foreground">{order.userPhone}</p>
  456. </div>
  457. </TableCell>
  458. <TableCell>
  459. <div>
  460. <p>{order.recevierName || '-'}</p>
  461. <p className="text-sm text-muted-foreground">{order.receiverMobile}</p>
  462. </div>
  463. </TableCell>
  464. <TableCell>
  465. <div>
  466. <p className="font-medium">{formatAmount(order.payAmount)}</p>
  467. <p className="text-sm text-muted-foreground">{formatAmount(order.amount)}</p>
  468. </div>
  469. </TableCell>
  470. <TableCell>{getStatusBadge(order.state, 'order')}</TableCell>
  471. <TableCell>{getStatusBadge(order.payState, 'pay')}</TableCell>
  472. <TableCell>
  473. {format(new Date(order.createdAt), 'yyyy-MM-dd HH:mm')}
  474. </TableCell>
  475. <TableCell className="text-right">
  476. <div className="flex justify-end gap-2">
  477. <Button
  478. variant="ghost"
  479. size="icon"
  480. onClick={() => handleViewDetails(order)}
  481. data-testid="order-view-button"
  482. >
  483. <Eye className="h-4 w-4" />
  484. </Button>
  485. <Button
  486. variant="ghost"
  487. size="icon"
  488. onClick={() => handleEditOrder(order)}
  489. data-testid="order-edit-button"
  490. >
  491. <Edit className="h-4 w-4" />
  492. </Button>
  493. {order.state === 0 && order.payState === 2 && (
  494. <Button
  495. variant="ghost"
  496. size="icon"
  497. onClick={() => handleDeliveryOrder(order)}
  498. data-testid="order-delivery-button"
  499. >
  500. <Truck className="h-4 w-4" />
  501. </Button>
  502. )}
  503. </div>
  504. </TableCell>
  505. </TableRow>
  506. ))}
  507. </TableBody>
  508. </Table>
  509. </div>
  510. {data?.data.length === 0 && !isLoading && (
  511. <div className="text-center py-8">
  512. <p className="text-muted-foreground">暂无订单数据</p>
  513. </div>
  514. )}
  515. <DataTablePagination
  516. currentPage={searchParams.page}
  517. pageSize={searchParams.limit}
  518. totalCount={data?.pagination.total || 0}
  519. onPageChange={(page, limit) => setSearchParams(prev => ({ ...prev, page, limit }))}
  520. />
  521. </CardContent>
  522. </Card>
  523. {/* 编辑订单模态框 */}
  524. <Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
  525. <DialogContent className="sm:max-w-[500px] max-h-[90vh] overflow-y-auto">
  526. <DialogHeader>
  527. <DialogTitle>编辑订单</DialogTitle>
  528. <DialogDescription>更新订单状态和备注信息</DialogDescription>
  529. </DialogHeader>
  530. <Form {...updateForm}>
  531. <form onSubmit={updateForm.handleSubmit(handleUpdateSubmit)} className="space-y-4">
  532. <FormField
  533. control={updateForm.control}
  534. name="state"
  535. render={({ field }) => (
  536. <FormItem>
  537. <FormLabel>订单状态</FormLabel>
  538. <Select onValueChange={(value) => field.onChange(parseInt(value))} value={field.value?.toString()}>
  539. <FormControl>
  540. <SelectTrigger data-testid="edit-order-status-select">
  541. <SelectValue placeholder="选择订单状态" />
  542. </SelectTrigger>
  543. </FormControl>
  544. <SelectContent>
  545. <SelectItem value="0">未发货</SelectItem>
  546. <SelectItem value="1">已发货</SelectItem>
  547. <SelectItem value="2">收货成功</SelectItem>
  548. <SelectItem value="3">已退货</SelectItem>
  549. </SelectContent>
  550. </Select>
  551. <FormMessage />
  552. </FormItem>
  553. )}
  554. />
  555. <FormField
  556. control={updateForm.control}
  557. name="payState"
  558. render={({ field }) => (
  559. <FormItem>
  560. <FormLabel>支付状态</FormLabel>
  561. <Select onValueChange={(value) => field.onChange(parseInt(value))} value={field.value?.toString()}>
  562. <FormControl>
  563. <SelectTrigger data-testid="edit-pay-status-select">
  564. <SelectValue placeholder="选择支付状态" />
  565. </SelectTrigger>
  566. </FormControl>
  567. <SelectContent>
  568. <SelectItem value="0">未支付</SelectItem>
  569. <SelectItem value="1">支付中</SelectItem>
  570. <SelectItem value="2">支付成功</SelectItem>
  571. <SelectItem value="3">已退款</SelectItem>
  572. <SelectItem value="4">支付失败</SelectItem>
  573. <SelectItem value="5">订单关闭</SelectItem>
  574. </SelectContent>
  575. </Select>
  576. <FormMessage />
  577. </FormItem>
  578. )}
  579. />
  580. <FormField
  581. control={updateForm.control}
  582. name="remark"
  583. render={({ field }) => (
  584. <FormItem>
  585. <FormLabel>客户备注</FormLabel>
  586. <FormControl>
  587. <Textarea
  588. placeholder="输入客户备注信息..."
  589. className="resize-none"
  590. data-testid="edit-remark-textarea"
  591. {...field}
  592. />
  593. </FormControl>
  594. <FormMessage />
  595. </FormItem>
  596. )}
  597. />
  598. <DialogFooter>
  599. <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
  600. 取消
  601. </Button>
  602. <Button type="submit" data-testid="order-save-button">保存</Button>
  603. </DialogFooter>
  604. </form>
  605. </Form>
  606. </DialogContent>
  607. </Dialog>
  608. {/* 订单详情模态框 */}
  609. <Dialog open={detailModalOpen} onOpenChange={setDetailModalOpen}>
  610. <DialogContent className="sm:max-w-[700px] max-h-[90vh] overflow-y-auto">
  611. <DialogHeader>
  612. <DialogTitle>订单详情</DialogTitle>
  613. <DialogDescription>查看订单的详细信息</DialogDescription>
  614. </DialogHeader>
  615. {selectedOrder && (
  616. <div className="space-y-4">
  617. <div className="grid grid-cols-2 gap-4">
  618. <div>
  619. <h4 className="font-medium mb-2">订单信息</h4>
  620. <div className="space-y-2 text-sm">
  621. <div className="flex justify-between">
  622. <span className="text-muted-foreground">订单号:</span>
  623. <span>{selectedOrder.orderNo}</span>
  624. </div>
  625. <div className="flex justify-between">
  626. <span className="text-muted-foreground">订单类型:</span>
  627. <span>{orderTypeMap[selectedOrder.orderType as keyof typeof orderTypeMap]?.label}</span>
  628. </div>
  629. <div className="flex justify-between">
  630. <span className="text-muted-foreground">订单金额:</span>
  631. <span>{formatAmount(selectedOrder.amount)}</span>
  632. </div>
  633. <div className="flex justify-between">
  634. <span className="text-muted-foreground">实付金额:</span>
  635. <span>{formatAmount(selectedOrder.payAmount)}</span>
  636. </div>
  637. <div className="flex justify-between">
  638. <span className="text-muted-foreground">运费:</span>
  639. <span>{formatAmount(selectedOrder.freightAmount)}</span>
  640. </div>
  641. </div>
  642. </div>
  643. <div>
  644. <h4 className="font-medium mb-2">状态信息</h4>
  645. <div className="space-y-2 text-sm">
  646. <div className="flex justify-between">
  647. <span className="text-muted-foreground">订单状态:</span>
  648. <span>{getStatusBadge(selectedOrder.state, 'order')}</span>
  649. </div>
  650. <div className="flex justify-between">
  651. <span className="text-muted-foreground">支付状态:</span>
  652. <span>{getStatusBadge(selectedOrder.payState, 'pay')}</span>
  653. </div>
  654. <div className="flex justify-between">
  655. <span className="text-muted-foreground">支付方式:</span>
  656. <span>{selectedOrder.payType === 1 ? '积分' : selectedOrder.payType === 2 ? '礼券' : '未选择'}</span>
  657. </div>
  658. <div className="flex justify-between">
  659. <span className="text-muted-foreground">创建时间:</span>
  660. <span>{format(new Date(selectedOrder.createdAt), 'yyyy-MM-dd HH:mm')}</span>
  661. </div>
  662. </div>
  663. </div>
  664. </div>
  665. <div className="grid grid-cols-2 gap-4">
  666. <div>
  667. <h4 className="font-medium mb-2">用户信息</h4>
  668. <div className="space-y-2 text-sm">
  669. <div className="flex justify-between">
  670. <span className="text-muted-foreground">用户名:</span>
  671. <span>{selectedOrder.user?.username || '-'}</span>
  672. </div>
  673. <div className="flex justify-between">
  674. <span className="text-muted-foreground">手机号:</span>
  675. <span>{selectedOrder.userPhone || '-'}</span>
  676. </div>
  677. </div>
  678. </div>
  679. <div>
  680. <h4 className="font-medium mb-2">收货信息</h4>
  681. <div className="space-y-2 text-sm">
  682. <div className="flex justify-between">
  683. <span className="text-muted-foreground">收货人:</span>
  684. <span>{selectedOrder.recevierName || '-'}</span>
  685. </div>
  686. <div className="flex justify-between">
  687. <span className="text-muted-foreground">手机号:</span>
  688. <span>{selectedOrder.receiverMobile || '-'}</span>
  689. </div>
  690. <div className="flex justify-between">
  691. <span className="text-muted-foreground">地址:</span>
  692. <span>{selectedOrder.address || '-'}</span>
  693. </div>
  694. </div>
  695. </div>
  696. </div>
  697. {/* 订单商品信息 */}
  698. <div>
  699. <h4 className="font-medium mb-3 flex items-center gap-2">
  700. <Package className="h-4 w-4" />
  701. 订单商品
  702. </h4>
  703. <div className="border rounded-md overflow-hidden">
  704. <div className="bg-muted px-4 py-2 border-b">
  705. <div className="grid grid-cols-12 gap-4 text-sm font-medium">
  706. <div className="col-span-5">商品信息</div>
  707. <div className="col-span-2 text-center">单价</div>
  708. <div className="col-span-2 text-center">数量</div>
  709. <div className="col-span-3 text-right">小计</div>
  710. </div>
  711. </div>
  712. <div className="divide-y">
  713. {selectedOrder.orderGoods?.map((item, index) => (
  714. <div key={item.id || index} className="px-4 py-3">
  715. <div className="grid grid-cols-12 gap-4 items-center">
  716. <div className="col-span-5 flex items-center gap-3">
  717. {item.imageFile && (
  718. <img
  719. src={item.imageFile.fullUrl}
  720. alt={item.goodsName}
  721. className="w-12 h-12 rounded-md object-cover"
  722. />
  723. )}
  724. <div>
  725. <p className="font-medium text-sm">{item.goodsName}</p>
  726. </div>
  727. </div>
  728. <div className="col-span-2 text-center text-sm">
  729. {formatAmount(item.price)}
  730. </div>
  731. <div className="col-span-2 text-center text-sm">
  732. {item.num}
  733. </div>
  734. <div className="col-span-3 text-right text-sm font-medium">
  735. {formatAmount(item.price * item.num)}
  736. </div>
  737. </div>
  738. </div>
  739. ))}
  740. </div>
  741. {selectedOrder.orderGoods?.length === 0 && (
  742. <div className="text-center py-8 text-muted-foreground">
  743. 暂无商品信息
  744. </div>
  745. )}
  746. </div>
  747. </div>
  748. {selectedOrder.remark && (
  749. <div>
  750. <h4 className="font-medium mb-2">客户备注</h4>
  751. <p className="text-sm bg-muted p-3 rounded-md">{selectedOrder.remark}</p>
  752. </div>
  753. )}
  754. </div>
  755. )}
  756. </DialogContent>
  757. </Dialog>
  758. {/* 发货模态框 */}
  759. <Dialog open={deliveryModalOpen} onOpenChange={setDeliveryModalOpen}>
  760. <DialogContent className="sm:max-w-[500px] max-h-[90vh] overflow-y-auto">
  761. <DialogHeader>
  762. <DialogTitle>订单发货</DialogTitle>
  763. <DialogDescription>选择发货方式并填写发货信息</DialogDescription>
  764. </DialogHeader>
  765. {deliveringOrder && (
  766. <div className="space-y-4">
  767. <div className="bg-muted p-3 rounded-md">
  768. <h4 className="font-medium mb-2">订单信息</h4>
  769. <div className="text-sm space-y-1">
  770. <div className="flex justify-between">
  771. <span className="text-muted-foreground">订单号:</span>
  772. <span>{deliveringOrder.orderNo}</span>
  773. </div>
  774. <div className="flex justify-between">
  775. <span className="text-muted-foreground">收货人:</span>
  776. <span>{deliveringOrder.recevierName || '-'}</span>
  777. </div>
  778. <div className="flex justify-between">
  779. <span className="text-muted-foreground">手机号:</span>
  780. <span>{deliveringOrder.receiverMobile || '-'}</span>
  781. </div>
  782. <div className="flex justify-between">
  783. <span className="text-muted-foreground">地址:</span>
  784. <span>{deliveringOrder.address || '-'}</span>
  785. </div>
  786. </div>
  787. </div>
  788. <Form {...deliveryForm}>
  789. <form onSubmit={deliveryForm.handleSubmit(handleDeliverySubmit)} className="space-y-4">
  790. <FormField
  791. control={deliveryForm.control}
  792. name="deliveryType"
  793. render={({ field }) => (
  794. <FormItem>
  795. <FormLabel>发货方式</FormLabel>
  796. <Select onValueChange={(value) => field.onChange(parseInt(value))} value={field.value?.toString()}>
  797. <FormControl>
  798. <SelectTrigger data-testid="delivery-type-select">
  799. <SelectValue placeholder="选择发货方式" />
  800. </SelectTrigger>
  801. </FormControl>
  802. <SelectContent>
  803. <SelectItem value="1">物流快递</SelectItem>
  804. <SelectItem value="2">同城配送</SelectItem>
  805. <SelectItem value="3">用户自提</SelectItem>
  806. <SelectItem value="4">虚拟发货</SelectItem>
  807. </SelectContent>
  808. </Select>
  809. <FormMessage />
  810. </FormItem>
  811. )}
  812. />
  813. {deliveryForm.watch('deliveryType') === 1 && (
  814. <>
  815. <FormField
  816. control={deliveryForm.control}
  817. name="deliveryCompany"
  818. render={({ field }) => (
  819. <FormItem>
  820. <FormLabel>快递公司</FormLabel>
  821. <FormControl>
  822. <Input
  823. placeholder="输入快递公司名称"
  824. data-testid="delivery-company-input"
  825. {...field}
  826. />
  827. </FormControl>
  828. <FormMessage />
  829. </FormItem>
  830. )}
  831. />
  832. <FormField
  833. control={deliveryForm.control}
  834. name="deliveryNo"
  835. render={({ field }) => (
  836. <FormItem>
  837. <FormLabel>快递单号</FormLabel>
  838. <FormControl>
  839. <Input
  840. placeholder="输入快递单号"
  841. data-testid="delivery-no-input"
  842. {...field}
  843. />
  844. </FormControl>
  845. <FormMessage />
  846. </FormItem>
  847. )}
  848. />
  849. </>
  850. )}
  851. <FormField
  852. control={deliveryForm.control}
  853. name="deliveryRemark"
  854. render={({ field }) => (
  855. <FormItem>
  856. <FormLabel>发货备注</FormLabel>
  857. <FormControl>
  858. <Textarea
  859. placeholder="输入发货备注信息..."
  860. className="resize-none"
  861. data-testid="delivery-remark-textarea"
  862. {...field}
  863. />
  864. </FormControl>
  865. <FormMessage />
  866. </FormItem>
  867. )}
  868. />
  869. <DialogFooter>
  870. <Button type="button" variant="outline" onClick={() => setDeliveryModalOpen(false)}>
  871. 取消
  872. </Button>
  873. <Button type="submit" data-testid="delivery-submit-button">确认发货</Button>
  874. </DialogFooter>
  875. </form>
  876. </Form>
  877. </div>
  878. )}
  879. </DialogContent>
  880. </Dialog>
  881. </div>
  882. );
  883. };