| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939 |
- import { useState } from 'react';
- import { useQuery } from '@tanstack/react-query';
- import { useForm } from 'react-hook-form';
- import { zodResolver } from '@hookform/resolvers/zod';
- import { format } from 'date-fns';
- import { toast } from 'sonner';
- import { Search, Edit, Eye, Package, Truck } from 'lucide-react';
- // 使用共享UI组件包的具体路径导入
- import { Button } from '@d8d/shared-ui-components/components/ui/button';
- import { Input } from '@d8d/shared-ui-components/components/ui/input';
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@d8d/shared-ui-components/components/ui/card';
- import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@d8d/shared-ui-components/components/ui/table';
- import { Badge } from '@d8d/shared-ui-components/components/ui/badge';
- import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@d8d/shared-ui-components/components/ui/dialog';
- import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@d8d/shared-ui-components/components/ui/form';
- import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@d8d/shared-ui-components/components/ui/select';
- import { Textarea } from '@d8d/shared-ui-components/components/ui/textarea';
- import { Skeleton } from '@d8d/shared-ui-components/components/ui/skeleton';
- // 简单分页组件
- const DataTablePagination = ({
- currentPage,
- pageSize,
- totalCount,
- onPageChange
- }: {
- currentPage: number;
- pageSize: number;
- totalCount: number;
- onPageChange: (page: number, limit: number) => void;
- }) => {
- const totalPages = Math.ceil(totalCount / pageSize);
- return (
- <div className="flex items-center justify-between px-2 py-4">
- <div className="text-sm text-muted-foreground">
- 共 {totalCount} 条记录
- </div>
- <div className="flex items-center space-x-2">
- <Button
- variant="outline"
- size="sm"
- onClick={() => onPageChange(Math.max(1, currentPage - 1), pageSize)}
- disabled={currentPage <= 1}
- >
- 上一页
- </Button>
- <div className="text-sm">
- 第 {currentPage} 页,共 {totalPages} 页
- </div>
- <Button
- variant="outline"
- size="sm"
- onClick={() => onPageChange(Math.min(totalPages, currentPage + 1), pageSize)}
- disabled={currentPage >= totalPages}
- >
- 下一页
- </Button>
- </div>
- </div>
- );
- };
- import { adminOrderClient, orderClientManager } from '../api';
- import type { InferResponseType } from 'hono/client';
- import { UpdateOrderDto } from '@d8d/orders-module-mt/schemas';
- // 类型定义
- type OrderResponse = InferResponseType<typeof adminOrderClient.index.$get, 200>['data'][0];
- type UpdateRequest = any;
- type DeliveryRequest = any;
- type DeliveryResponse = any;
- // 状态映射
- const orderStatusMap = {
- 0: { label: '未发货', color: 'warning' },
- 1: { label: '已发货', color: 'info' },
- 2: { label: '收货成功', color: 'success' },
- 3: { label: '已退货', color: 'destructive' },
- } as const;
- const payStatusMap = {
- 0: { label: '未支付', color: 'warning' },
- 1: { label: '支付中', color: 'info' },
- 2: { label: '支付成功', color: 'success' },
- 3: { label: '已退款', color: 'secondary' },
- 4: { label: '支付失败', color: 'destructive' },
- 5: { label: '订单关闭', color: 'destructive' },
- } as const;
- const orderTypeMap = {
- 1: { label: '实物订单', color: 'default' },
- 2: { label: '虚拟订单', color: 'secondary' },
- } as const;
- const deliveryTypeMap = {
- 0: { label: '未发货', color: 'warning' },
- 1: { label: '物流快递', color: 'info' },
- 2: { label: '同城配送', color: 'info' },
- 3: { label: '用户自提', color: 'info' },
- 4: { label: '虚拟发货', color: 'info' },
- } as const;
- export const OrderManagement = () => {
- const [searchParams, setSearchParams] = useState({
- page: 1,
- limit: 10,
- search: '',
- status: 'all',
- payStatus: 'all',
- });
- const [isModalOpen, setIsModalOpen] = useState(false);
- const [editingOrder, setEditingOrder] = useState<OrderResponse | null>(null);
- const [detailModalOpen, setDetailModalOpen] = useState(false);
- const [selectedOrder, setSelectedOrder] = useState<OrderResponse | null>(null);
- const [deliveryModalOpen, setDeliveryModalOpen] = useState(false);
- const [deliveringOrder, setDeliveringOrder] = useState<OrderResponse | null>(null);
- // 表单实例
- const updateForm = useForm<UpdateRequest>({
- resolver: zodResolver(UpdateOrderDto),
- defaultValues: {},
- });
- // 发货表单实例
- const deliveryForm = useForm<DeliveryRequest>({
- defaultValues: {
- deliveryType: 1,
- deliveryCompany: '',
- deliveryNo: '',
- deliveryRemark: '',
- },
- });
- // 数据查询 - 60秒自动刷新
- const { data, isLoading, refetch } = useQuery({
- queryKey: ['orders', searchParams],
- queryFn: async () => {
- const filters: any = {};
- if (searchParams.status !== 'all') {
- filters.state = parseInt(searchParams.status);
- }
- if (searchParams.payStatus !== 'all') {
- filters.payState = parseInt(searchParams.payStatus);
- }
- const res = await orderClientManager.getAdminOrderClient().index.$get({
- query: {
- page: searchParams.page,
- pageSize: searchParams.limit,
- keyword: searchParams.search,
- ...(Object.keys(filters).length > 0 && { filters: JSON.stringify(filters) }),
- }
- });
- if (res.status !== 200) throw new Error('获取订单列表失败');
- return await res.json();
- },
- refetchInterval: 60000, // 60秒自动刷新
- refetchIntervalInBackground: false, // 只在页面可见时刷新
- staleTime: 30000, // 30秒后数据视为过期
- });
- // 处理搜索
- const handleSearch = () => {
- setSearchParams(prev => ({ ...prev, page: 1 }));
- };
- // 处理编辑订单
- const handleEditOrder = (order: OrderResponse) => {
- setEditingOrder(order);
- updateForm.reset({
- state: order.state,
- payState: order.payState,
- remark: order.remark || '',
- });
- setIsModalOpen(true);
- };
- // 处理查看详情
- const handleViewDetails = (order: OrderResponse) => {
- setSelectedOrder(order);
- setDetailModalOpen(true);
- };
- // 处理发货
- const handleDeliveryOrder = (order: OrderResponse) => {
- setDeliveringOrder(order);
- deliveryForm.reset({
- deliveryType: 1,
- deliveryCompany: '',
- deliveryNo: '',
- deliveryRemark: '',
- });
- setDeliveryModalOpen(true);
- };
- // 处理发货提交
- const handleDeliverySubmit = async (data: DeliveryRequest) => {
- if (!deliveringOrder || !deliveringOrder.id) return;
- try {
- const res = await (orderClientManager.getAdminDeliveryClient() as any)[':id']['delivery']['$post']({
- param: { id: deliveringOrder.id },
- json: data,
- });
- if (res.status === 200) {
- const result = await res.json() as DeliveryResponse;
- toast.success(result.message || '发货成功');
- setDeliveryModalOpen(false);
- refetch();
- } else {
- const error = await res.json();
- toast.error(error.message || '发货失败');
- }
- } catch (error) {
- console.error('发货失败:', error);
- toast.error('发货失败,请重试');
- }
- };
- // 处理更新订单
- const handleUpdateSubmit = async (data: UpdateRequest) => {
- if (!editingOrder || !editingOrder.id) return;
- try {
- const res = await (orderClientManager.getAdminOrderClient() as any)[':id']['$put']({
- param: { id: editingOrder.id },
- json: data,
- });
- if (res.status === 200) {
- toast.success('订单更新成功');
- setIsModalOpen(false);
- refetch();
- } else {
- const error = await res.json();
- toast.error(error.message || '更新失败');
- }
- } catch (error) {
- console.error('更新订单失败:', error);
- toast.error('更新失败,请重试');
- }
- };
- // 格式化金额
- const formatAmount = (amount: number) => {
- return `¥${amount.toFixed(2)}`;
- };
- // 获取状态颜色
- const getStatusBadge = (status: number, type: 'order' | 'pay' | 'delivery') => {
- const map = type === 'order' ? orderStatusMap : type === 'pay' ? payStatusMap : deliveryTypeMap;
- const config = map[status as keyof typeof map] || { label: '未知', color: 'default' };
- return <Badge variant={config.color as any}>{config.label}</Badge>;
- };
- // 骨架屏 - 只覆盖表格区域,搜索区域保持可用
- if (isLoading) {
- return (
- <div className="space-y-4">
- {/* 页面标题 */}
- <div className="flex justify-between items-center">
- <div>
- <h1 className="text-2xl font-bold">订单管理</h1>
- <p className="text-muted-foreground">管理所有订单信息</p>
- </div>
- </div>
- {/* 搜索区域 - 保持可用 */}
- <Card>
- <CardHeader>
- <CardTitle>订单列表</CardTitle>
- <CardDescription>查看和管理所有订单</CardDescription>
- </CardHeader>
- <CardContent>
- <div className="flex gap-4 mb-4">
- <div className="relative flex-1 max-w-sm">
- <Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
- <Input
- placeholder="搜索订单号、手机号、收货人姓名..."
- value={searchParams.search}
- onChange={(e) => setSearchParams(prev => ({ ...prev, search: e.target.value }))}
- className="pl-8"
- data-testid="order-search-input"
- />
- </div>
- <Select
- value={searchParams.status}
- onValueChange={(value) => setSearchParams(prev => ({ ...prev, status: value, page: 1 }))}
- >
- <SelectTrigger className="w-32" data-testid="order-status-select">
- <SelectValue placeholder="订单状态" />
- </SelectTrigger>
- <SelectContent>
- <SelectItem value="all">全部</SelectItem>
- <SelectItem value="0">未发货</SelectItem>
- <SelectItem value="1">已发货</SelectItem>
- <SelectItem value="2">收货成功</SelectItem>
- <SelectItem value="3">已退货</SelectItem>
- </SelectContent>
- </Select>
- <Select
- value={searchParams.payStatus}
- onValueChange={(value) => setSearchParams(prev => ({ ...prev, payStatus: value, page: 1 }))}
- >
- <SelectTrigger className="w-32" data-testid="order-pay-status-select">
- <SelectValue placeholder="支付状态" />
- </SelectTrigger>
- <SelectContent>
- <SelectItem value="all">全部</SelectItem>
- <SelectItem value="0">未支付</SelectItem>
- <SelectItem value="1">支付中</SelectItem>
- <SelectItem value="2">支付成功</SelectItem>
- <SelectItem value="3">已退款</SelectItem>
- <SelectItem value="4">支付失败</SelectItem>
- <SelectItem value="5">订单关闭</SelectItem>
- </SelectContent>
- </Select>
- <Button onClick={handleSearch} data-testid="order-search-button">
- <Search className="h-4 w-4 mr-2" />
- 搜索
- </Button>
- </div>
- {/* 表格骨架屏 */}
- <div className="rounded-md border">
- <Table>
- <TableHeader>
- <TableRow>
- <TableHead>订单号</TableHead>
- <TableHead>用户信息</TableHead>
- <TableHead>收货人</TableHead>
- <TableHead>金额</TableHead>
- <TableHead>订单状态</TableHead>
- <TableHead>支付状态</TableHead>
- <TableHead>创建时间</TableHead>
- <TableHead className="text-right">操作</TableHead>
- </TableRow>
- </TableHeader>
- <TableBody>
- {[...Array(5)].map((_, i) => (
- <TableRow key={i}>
- <TableCell>
- <Skeleton className="h-4 w-32" />
- </TableCell>
- <TableCell>
- <Skeleton className="h-4 w-24" />
- </TableCell>
- <TableCell>
- <Skeleton className="h-4 w-20" />
- </TableCell>
- <TableCell>
- <Skeleton className="h-4 w-16" />
- </TableCell>
- <TableCell>
- <Skeleton className="h-6 w-16" />
- </TableCell>
- <TableCell>
- <Skeleton className="h-6 w-16" />
- </TableCell>
- <TableCell>
- <Skeleton className="h-4 w-24" />
- </TableCell>
- <TableCell className="text-right">
- <div className="flex justify-end gap-2">
- <Skeleton className="h-8 w-8" />
- <Skeleton className="h-8 w-8" />
- </div>
- </TableCell>
- </TableRow>
- ))}
- </TableBody>
- </Table>
- </div>
- {/* 分页骨架屏 */}
- <div className="flex items-center justify-between px-2 py-4">
- <Skeleton className="h-4 w-32" />
- <div className="flex items-center space-x-2">
- <Skeleton className="h-8 w-16" />
- <Skeleton className="h-4 w-24" />
- <Skeleton className="h-8 w-16" />
- </div>
- </div>
- </CardContent>
- </Card>
- </div>
- );
- }
- return (
- <div className="space-y-4">
- {/* 页面标题 */}
- <div className="flex justify-between items-center">
- <div>
- <h1 className="text-2xl font-bold">订单管理</h1>
- <p className="text-muted-foreground">管理所有订单信息</p>
- </div>
- </div>
- {/* 搜索区域 */}
- <Card>
- <CardHeader>
- <CardTitle>订单列表</CardTitle>
- <CardDescription>查看和管理所有订单</CardDescription>
- </CardHeader>
- <CardContent>
- <div className="flex gap-4 mb-4">
- <div className="relative flex-1 max-w-sm">
- <Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
- <Input
- placeholder="搜索订单号、手机号、收货人姓名..."
- value={searchParams.search}
- onChange={(e) => setSearchParams(prev => ({ ...prev, search: e.target.value }))}
- className="pl-8"
- data-testid="order-search-input"
- />
- </div>
- <Select
- value={searchParams.status}
- onValueChange={(value) => setSearchParams(prev => ({ ...prev, status: value, page: 1 }))}
- >
- <SelectTrigger className="w-32" data-testid="order-status-select">
- <SelectValue placeholder="订单状态" />
- </SelectTrigger>
- <SelectContent>
- <SelectItem value="all">全部</SelectItem>
- <SelectItem value="0">未发货</SelectItem>
- <SelectItem value="1">已发货</SelectItem>
- <SelectItem value="2">收货成功</SelectItem>
- <SelectItem value="3">已退货</SelectItem>
- </SelectContent>
- </Select>
- <Select
- value={searchParams.payStatus}
- onValueChange={(value) => setSearchParams(prev => ({ ...prev, payStatus: value, page: 1 }))}
- >
- <SelectTrigger className="w-32" data-testid="order-pay-status-select">
- <SelectValue placeholder="支付状态" />
- </SelectTrigger>
- <SelectContent>
- <SelectItem value="all">全部</SelectItem>
- <SelectItem value="0">未支付</SelectItem>
- <SelectItem value="1">支付中</SelectItem>
- <SelectItem value="2">支付成功</SelectItem>
- <SelectItem value="3">已退款</SelectItem>
- <SelectItem value="4">支付失败</SelectItem>
- <SelectItem value="5">订单关闭</SelectItem>
- </SelectContent>
- </Select>
- <Button onClick={handleSearch} data-testid="order-search-button">
- <Search className="h-4 w-4 mr-2" />
- 搜索
- </Button>
- </div>
- {/* 数据表格 */}
- <div className="rounded-md border">
- <Table>
- <TableHeader>
- <TableRow>
- <TableHead>订单号</TableHead>
- <TableHead>用户信息</TableHead>
- <TableHead>收货人</TableHead>
- <TableHead>金额</TableHead>
- <TableHead>订单状态</TableHead>
- <TableHead>支付状态</TableHead>
- <TableHead>创建时间</TableHead>
- <TableHead className="text-right">操作</TableHead>
- </TableRow>
- </TableHeader>
- <TableBody>
- {data?.data.map((order) => (
- <TableRow key={order.id}>
- <TableCell>
- <div>
- <p className="font-medium">{order.orderNo}</p>
- <p className="text-sm text-muted-foreground">
- {orderTypeMap[order.orderType as keyof typeof orderTypeMap]?.label}
- </p>
- </div>
- </TableCell>
- <TableCell>
- <div>
- <p>{order.user?.username || '-'}</p>
- <p className="text-sm text-muted-foreground">{order.userPhone}</p>
- </div>
- </TableCell>
- <TableCell>
- <div>
- <p>{order.recevierName || '-'}</p>
- <p className="text-sm text-muted-foreground">{order.receiverMobile}</p>
- </div>
- </TableCell>
- <TableCell>
- <div>
- <p className="font-medium">{formatAmount(order.payAmount)}</p>
- <p className="text-sm text-muted-foreground">{formatAmount(order.amount)}</p>
- </div>
- </TableCell>
- <TableCell>{getStatusBadge(order.state, 'order')}</TableCell>
- <TableCell>{getStatusBadge(order.payState, 'pay')}</TableCell>
- <TableCell>
- {format(new Date(order.createdAt), 'yyyy-MM-dd HH:mm')}
- </TableCell>
- <TableCell className="text-right">
- <div className="flex justify-end gap-2">
- <Button
- variant="ghost"
- size="icon"
- onClick={() => handleViewDetails(order)}
- data-testid="order-view-button"
- >
- <Eye className="h-4 w-4" />
- </Button>
- <Button
- variant="ghost"
- size="icon"
- onClick={() => handleEditOrder(order)}
- data-testid="order-edit-button"
- >
- <Edit className="h-4 w-4" />
- </Button>
- {order.state === 0 && order.payState === 2 && (
- <Button
- variant="ghost"
- size="icon"
- onClick={() => handleDeliveryOrder(order)}
- data-testid="order-delivery-button"
- >
- <Truck className="h-4 w-4" />
- </Button>
- )}
- </div>
- </TableCell>
- </TableRow>
- ))}
- </TableBody>
- </Table>
- </div>
- {data?.data.length === 0 && !isLoading && (
- <div className="text-center py-8">
- <p className="text-muted-foreground">暂无订单数据</p>
- </div>
- )}
- <DataTablePagination
- currentPage={searchParams.page}
- pageSize={searchParams.limit}
- totalCount={data?.pagination.total || 0}
- onPageChange={(page, limit) => setSearchParams(prev => ({ ...prev, page, limit }))}
- />
- </CardContent>
- </Card>
- {/* 编辑订单模态框 */}
- <Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
- <DialogContent className="sm:max-w-[500px] max-h-[90vh] overflow-y-auto">
- <DialogHeader>
- <DialogTitle>编辑订单</DialogTitle>
- <DialogDescription>更新订单状态和备注信息</DialogDescription>
- </DialogHeader>
- <Form {...updateForm}>
- <form onSubmit={updateForm.handleSubmit(handleUpdateSubmit)} className="space-y-4">
- <FormField
- control={updateForm.control}
- name="state"
- render={({ field }) => (
- <FormItem>
- <FormLabel>订单状态</FormLabel>
- <Select onValueChange={(value) => field.onChange(parseInt(value))} value={field.value?.toString()}>
- <FormControl>
- <SelectTrigger data-testid="edit-order-status-select">
- <SelectValue placeholder="选择订单状态" />
- </SelectTrigger>
- </FormControl>
- <SelectContent>
- <SelectItem value="0">未发货</SelectItem>
- <SelectItem value="1">已发货</SelectItem>
- <SelectItem value="2">收货成功</SelectItem>
- <SelectItem value="3">已退货</SelectItem>
- </SelectContent>
- </Select>
- <FormMessage />
- </FormItem>
- )}
- />
- <FormField
- control={updateForm.control}
- name="payState"
- render={({ field }) => (
- <FormItem>
- <FormLabel>支付状态</FormLabel>
- <Select onValueChange={(value) => field.onChange(parseInt(value))} value={field.value?.toString()}>
- <FormControl>
- <SelectTrigger data-testid="edit-pay-status-select">
- <SelectValue placeholder="选择支付状态" />
- </SelectTrigger>
- </FormControl>
- <SelectContent>
- <SelectItem value="0">未支付</SelectItem>
- <SelectItem value="1">支付中</SelectItem>
- <SelectItem value="2">支付成功</SelectItem>
- <SelectItem value="3">已退款</SelectItem>
- <SelectItem value="4">支付失败</SelectItem>
- <SelectItem value="5">订单关闭</SelectItem>
- </SelectContent>
- </Select>
- <FormMessage />
- </FormItem>
- )}
- />
- <FormField
- control={updateForm.control}
- name="remark"
- render={({ field }) => (
- <FormItem>
- <FormLabel>客户备注</FormLabel>
- <FormControl>
- <Textarea
- placeholder="输入客户备注信息..."
- className="resize-none"
- data-testid="edit-remark-textarea"
- {...field}
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- <DialogFooter>
- <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
- 取消
- </Button>
- <Button type="submit" data-testid="order-save-button">保存</Button>
- </DialogFooter>
- </form>
- </Form>
- </DialogContent>
- </Dialog>
- {/* 订单详情模态框 */}
- <Dialog open={detailModalOpen} onOpenChange={setDetailModalOpen}>
- <DialogContent className="sm:max-w-[700px] max-h-[90vh] overflow-y-auto">
- <DialogHeader>
- <DialogTitle>订单详情</DialogTitle>
- <DialogDescription>查看订单的详细信息</DialogDescription>
- </DialogHeader>
- {selectedOrder && (
- <div className="space-y-4">
- <div className="grid grid-cols-2 gap-4">
- <div>
- <h4 className="font-medium mb-2">订单信息</h4>
- <div className="space-y-2 text-sm">
- <div className="flex justify-between">
- <span className="text-muted-foreground">订单号:</span>
- <span>{selectedOrder.orderNo}</span>
- </div>
- <div className="flex justify-between">
- <span className="text-muted-foreground">订单类型:</span>
- <span>{orderTypeMap[selectedOrder.orderType as keyof typeof orderTypeMap]?.label}</span>
- </div>
- <div className="flex justify-between">
- <span className="text-muted-foreground">订单金额:</span>
- <span>{formatAmount(selectedOrder.amount)}</span>
- </div>
- <div className="flex justify-between">
- <span className="text-muted-foreground">实付金额:</span>
- <span>{formatAmount(selectedOrder.payAmount)}</span>
- </div>
- <div className="flex justify-between">
- <span className="text-muted-foreground">运费:</span>
- <span>{formatAmount(selectedOrder.freightAmount)}</span>
- </div>
- </div>
- </div>
- <div>
- <h4 className="font-medium mb-2">状态信息</h4>
- <div className="space-y-2 text-sm">
- <div className="flex justify-between">
- <span className="text-muted-foreground">订单状态:</span>
- <span>{getStatusBadge(selectedOrder.state, 'order')}</span>
- </div>
- <div className="flex justify-between">
- <span className="text-muted-foreground">支付状态:</span>
- <span>{getStatusBadge(selectedOrder.payState, 'pay')}</span>
- </div>
- <div className="flex justify-between">
- <span className="text-muted-foreground">支付方式:</span>
- <span>{selectedOrder.payType === 1 ? '积分' : selectedOrder.payType === 2 ? '礼券' : '未选择'}</span>
- </div>
- <div className="flex justify-between">
- <span className="text-muted-foreground">创建时间:</span>
- <span>{format(new Date(selectedOrder.createdAt), 'yyyy-MM-dd HH:mm')}</span>
- </div>
- </div>
- </div>
- </div>
- <div className="grid grid-cols-2 gap-4">
- <div>
- <h4 className="font-medium mb-2">用户信息</h4>
- <div className="space-y-2 text-sm">
- <div className="flex justify-between">
- <span className="text-muted-foreground">用户名:</span>
- <span>{selectedOrder.user?.username || '-'}</span>
- </div>
- <div className="flex justify-between">
- <span className="text-muted-foreground">手机号:</span>
- <span>{selectedOrder.userPhone || '-'}</span>
- </div>
- </div>
- </div>
- <div>
- <h4 className="font-medium mb-2">收货信息</h4>
- <div className="space-y-2 text-sm">
- <div className="flex justify-between">
- <span className="text-muted-foreground">收货人:</span>
- <span>{selectedOrder.recevierName || '-'}</span>
- </div>
- <div className="flex justify-between">
- <span className="text-muted-foreground">手机号:</span>
- <span>{selectedOrder.receiverMobile || '-'}</span>
- </div>
- <div className="flex justify-between">
- <span className="text-muted-foreground">地址:</span>
- <span>{selectedOrder.address || '-'}</span>
- </div>
- </div>
- </div>
- </div>
- {/* 订单商品信息 */}
- <div>
- <h4 className="font-medium mb-3 flex items-center gap-2">
- <Package className="h-4 w-4" />
- 订单商品
- </h4>
- <div className="border rounded-md overflow-hidden">
- <div className="bg-muted px-4 py-2 border-b">
- <div className="grid grid-cols-12 gap-4 text-sm font-medium">
- <div className="col-span-5">商品信息</div>
- <div className="col-span-2 text-center">单价</div>
- <div className="col-span-2 text-center">数量</div>
- <div className="col-span-3 text-right">小计</div>
- </div>
- </div>
- <div className="divide-y">
- {selectedOrder.orderGoods?.map((item, index) => (
- <div key={item.id || index} className="px-4 py-3">
- <div className="grid grid-cols-12 gap-4 items-center">
- <div className="col-span-5 flex items-center gap-3">
- {item.imageFile && (
- <img
- src={item.imageFile.fullUrl}
- alt={item.goodsName}
- className="w-12 h-12 rounded-md object-cover"
- />
- )}
- <div>
- <p className="font-medium text-sm">{item.goodsName}</p>
- </div>
- </div>
- <div className="col-span-2 text-center text-sm">
- {formatAmount(item.price)}
- </div>
- <div className="col-span-2 text-center text-sm">
- {item.num}
- </div>
- <div className="col-span-3 text-right text-sm font-medium">
- {formatAmount(item.price * item.num)}
- </div>
- </div>
- </div>
- ))}
- </div>
- {selectedOrder.orderGoods?.length === 0 && (
- <div className="text-center py-8 text-muted-foreground">
- 暂无商品信息
- </div>
- )}
- </div>
- </div>
- {selectedOrder.remark && (
- <div>
- <h4 className="font-medium mb-2">客户备注</h4>
- <p className="text-sm bg-muted p-3 rounded-md">{selectedOrder.remark}</p>
- </div>
- )}
- </div>
- )}
- </DialogContent>
- </Dialog>
- {/* 发货模态框 */}
- <Dialog open={deliveryModalOpen} onOpenChange={setDeliveryModalOpen}>
- <DialogContent className="sm:max-w-[500px] max-h-[90vh] overflow-y-auto">
- <DialogHeader>
- <DialogTitle>订单发货</DialogTitle>
- <DialogDescription>选择发货方式并填写发货信息</DialogDescription>
- </DialogHeader>
- {deliveringOrder && (
- <div className="space-y-4">
- <div className="bg-muted p-3 rounded-md">
- <h4 className="font-medium mb-2">订单信息</h4>
- <div className="text-sm space-y-1">
- <div className="flex justify-between">
- <span className="text-muted-foreground">订单号:</span>
- <span>{deliveringOrder.orderNo}</span>
- </div>
- <div className="flex justify-between">
- <span className="text-muted-foreground">收货人:</span>
- <span>{deliveringOrder.recevierName || '-'}</span>
- </div>
- <div className="flex justify-between">
- <span className="text-muted-foreground">手机号:</span>
- <span>{deliveringOrder.receiverMobile || '-'}</span>
- </div>
- <div className="flex justify-between">
- <span className="text-muted-foreground">地址:</span>
- <span>{deliveringOrder.address || '-'}</span>
- </div>
- </div>
- </div>
- <Form {...deliveryForm}>
- <form onSubmit={deliveryForm.handleSubmit(handleDeliverySubmit)} className="space-y-4">
- <FormField
- control={deliveryForm.control}
- name="deliveryType"
- render={({ field }) => (
- <FormItem>
- <FormLabel>发货方式</FormLabel>
- <Select onValueChange={(value) => field.onChange(parseInt(value))} value={field.value?.toString()}>
- <FormControl>
- <SelectTrigger data-testid="delivery-type-select">
- <SelectValue placeholder="选择发货方式" />
- </SelectTrigger>
- </FormControl>
- <SelectContent>
- <SelectItem value="1">物流快递</SelectItem>
- <SelectItem value="2">同城配送</SelectItem>
- <SelectItem value="3">用户自提</SelectItem>
- <SelectItem value="4">虚拟发货</SelectItem>
- </SelectContent>
- </Select>
- <FormMessage />
- </FormItem>
- )}
- />
- {deliveryForm.watch('deliveryType') === 1 && (
- <>
- <FormField
- control={deliveryForm.control}
- name="deliveryCompany"
- render={({ field }) => (
- <FormItem>
- <FormLabel>快递公司</FormLabel>
- <FormControl>
- <Input
- placeholder="输入快递公司名称"
- data-testid="delivery-company-input"
- {...field}
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- <FormField
- control={deliveryForm.control}
- name="deliveryNo"
- render={({ field }) => (
- <FormItem>
- <FormLabel>快递单号</FormLabel>
- <FormControl>
- <Input
- placeholder="输入快递单号"
- data-testid="delivery-no-input"
- {...field}
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- </>
- )}
- <FormField
- control={deliveryForm.control}
- name="deliveryRemark"
- render={({ field }) => (
- <FormItem>
- <FormLabel>发货备注</FormLabel>
- <FormControl>
- <Textarea
- placeholder="输入发货备注信息..."
- className="resize-none"
- data-testid="delivery-remark-textarea"
- {...field}
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- <DialogFooter>
- <Button type="button" variant="outline" onClick={() => setDeliveryModalOpen(false)}>
- 取消
- </Button>
- <Button type="submit" data-testid="delivery-submit-button">确认发货</Button>
- </DialogFooter>
- </form>
- </Form>
- </div>
- )}
- </DialogContent>
- </Dialog>
- </div>
- );
- };
|