| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685 |
- 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 } 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 } from '../api';
- import type { InferRequestType, InferResponseType } from 'hono/client';
- import { UpdateOrderDto } from '@d8d/orders-module/schemas';
- // 类型定义
- type OrderResponse = InferResponseType<typeof adminOrderClient.$get, 200>['data'][0];
- type UpdateRequest = InferRequestType<typeof adminOrderClient[':id']['$put']>['json'];
- // 状态映射
- 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;
- 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 updateForm = useForm<UpdateRequest>({
- resolver: zodResolver(UpdateOrderDto),
- defaultValues: {},
- });
- // 数据查询
- 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 adminOrderClient.$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();
- }
- });
- // 处理搜索
- 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 handleUpdateSubmit = async (data: UpdateRequest) => {
- if (!editingOrder) return;
- try {
- const res = await adminOrderClient[':id']['$put']({
- param: { id: editingOrder.id.toString() },
- 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') => {
- const map = type === 'order' ? orderStatusMap : payStatusMap;
- 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>
- </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>
- {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>
- </div>
- );
- };
|