|
@@ -1,7 +1,9 @@
|
|
|
import React, { useState, useMemo, useCallback } from 'react';
|
|
import React, { useState, useMemo, useCallback } from 'react';
|
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
|
import { format } from 'date-fns';
|
|
import { format } from 'date-fns';
|
|
|
-import { Search, Filter, X, Eye } from 'lucide-react';
|
|
|
|
|
|
|
+import { Search, Filter, X, Eye, Download } from 'lucide-react';
|
|
|
|
|
+import * as XLSX from 'xlsx';
|
|
|
|
|
+import { toast } from 'sonner';
|
|
|
import { orderClient } from '@/client/api';
|
|
import { orderClient } from '@/client/api';
|
|
|
import type { InferResponseType } from 'hono/client';
|
|
import type { InferResponseType } from 'hono/client';
|
|
|
import { Button } from '@/client/components/ui/button';
|
|
import { Button } from '@/client/components/ui/button';
|
|
@@ -31,6 +33,7 @@ export const OrdersPage = () => {
|
|
|
const [showFilters, setShowFilters] = useState(false);
|
|
const [showFilters, setShowFilters] = useState(false);
|
|
|
const [detailDialogOpen, setDetailDialogOpen] = useState(false);
|
|
const [detailDialogOpen, setDetailDialogOpen] = useState(false);
|
|
|
const [selectedOrder, setSelectedOrder] = useState<OrderResponse | null>(null);
|
|
const [selectedOrder, setSelectedOrder] = useState<OrderResponse | null>(null);
|
|
|
|
|
+ const [exporting, setExporting] = useState(false);
|
|
|
|
|
|
|
|
// 获取订单列表
|
|
// 获取订单列表
|
|
|
const { data: ordersData, isLoading } = useQuery({
|
|
const { data: ordersData, isLoading } = useQuery({
|
|
@@ -138,6 +141,104 @@ export const OrdersPage = () => {
|
|
|
setDetailDialogOpen(true);
|
|
setDetailDialogOpen(true);
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+ // 导出订单数据为Excel
|
|
|
|
|
+ const handleExportOrders = async () => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ setExporting(true);
|
|
|
|
|
+ toast.info('正在导出订单数据,请稍候...');
|
|
|
|
|
+
|
|
|
|
|
+ // 构建导出筛选条件
|
|
|
|
|
+ const exportFilters = {
|
|
|
|
|
+ status: filters.status,
|
|
|
|
|
+ paymentStatus: filters.paymentStatus,
|
|
|
|
|
+ keyword: searchParams.search,
|
|
|
|
|
+ page: 1,
|
|
|
|
|
+ pageSize: 10000 // 使用大pageSize获取所有数据
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 调用API获取订单数据
|
|
|
|
|
+ const res = await orderClient.$get({
|
|
|
|
|
+ query: {
|
|
|
|
|
+ page: exportFilters.page,
|
|
|
|
|
+ pageSize: exportFilters.pageSize,
|
|
|
|
|
+ keyword: exportFilters.keyword,
|
|
|
|
|
+ filters: JSON.stringify({
|
|
|
|
|
+ status: exportFilters.status,
|
|
|
|
|
+ paymentStatus: exportFilters.paymentStatus
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (res.status !== 200) {
|
|
|
|
|
+ throw new Error('获取订单数据失败');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const exportData = await res.json();
|
|
|
|
|
+ const orders = exportData.data || [];
|
|
|
|
|
+
|
|
|
|
|
+ if (orders.length === 0) {
|
|
|
|
|
+ toast.warning('没有找到符合条件的订单数据');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 准备Excel数据
|
|
|
|
|
+ const excelData = orders.map((order: OrderResponse) => ({
|
|
|
|
|
+ '订单编号': order.id,
|
|
|
|
|
+ '用户名': order.user?.username || '未知用户',
|
|
|
|
|
+ '手机号': order.user?.phone || '未知',
|
|
|
|
|
+ '路线名称': order.route?.name || '未知路线',
|
|
|
|
|
+ '路线描述': order.route?.description || '无描述',
|
|
|
|
|
+ '乘客数量': order.passengerCount,
|
|
|
|
|
+ '订单金额': `¥${order.totalAmount}`,
|
|
|
|
|
+ '订单状态': order.status,
|
|
|
|
|
+ '支付状态': order.paymentStatus,
|
|
|
|
|
+ '创建时间': format(new Date(order.createdAt), 'yyyy-MM-dd HH:mm:ss'),
|
|
|
|
|
+ '更新时间': format(new Date(order.updatedAt), 'yyyy-MM-dd HH:mm:ss'),
|
|
|
|
|
+ '乘客信息': order.passengerSnapshots?.map((passenger: any, index: number) =>
|
|
|
|
|
+ `乘客${index + 1}: ${passenger.name || '未知'} (${passenger.idType || '未知证件类型'}: ${passenger.idNumber || '未知证件号'}, 手机: ${passenger.phone || '未知'})`
|
|
|
|
|
+ ).join('; ') || '无乘客信息'
|
|
|
|
|
+ }));
|
|
|
|
|
+
|
|
|
|
|
+ // 创建工作簿和工作表
|
|
|
|
|
+ const wb = XLSX.utils.book_new();
|
|
|
|
|
+ const ws = XLSX.utils.json_to_sheet(excelData);
|
|
|
|
|
+
|
|
|
|
|
+ // 设置列宽
|
|
|
|
|
+ const colWidths = [
|
|
|
|
|
+ { wch: 15 }, // 订单编号
|
|
|
|
|
+ { wch: 12 }, // 用户名
|
|
|
|
|
+ { wch: 15 }, // 手机号
|
|
|
|
|
+ { wch: 20 }, // 路线名称
|
|
|
|
|
+ { wch: 30 }, // 路线描述
|
|
|
|
|
+ { wch: 10 }, // 乘客数量
|
|
|
|
|
+ { wch: 12 }, // 订单金额
|
|
|
|
|
+ { wch: 10 }, // 订单状态
|
|
|
|
|
+ { wch: 10 }, // 支付状态
|
|
|
|
|
+ { wch: 20 }, // 创建时间
|
|
|
|
|
+ { wch: 20 }, // 更新时间
|
|
|
|
|
+ { wch: 40 } // 乘客信息
|
|
|
|
|
+ ];
|
|
|
|
|
+ ws['!cols'] = colWidths;
|
|
|
|
|
+
|
|
|
|
|
+ // 添加工作表到工作簿
|
|
|
|
|
+ XLSX.utils.book_append_sheet(wb, ws, '订单数据');
|
|
|
|
|
+
|
|
|
|
|
+ // 生成文件名
|
|
|
|
|
+ const timestamp = format(new Date(), 'yyyyMMdd_HHmmss');
|
|
|
|
|
+ const fileName = `订单导出_${timestamp}.xlsx`;
|
|
|
|
|
+
|
|
|
|
|
+ // 导出Excel文件
|
|
|
|
|
+ XLSX.writeFile(wb, fileName);
|
|
|
|
|
+
|
|
|
|
|
+ toast.success(`成功导出 ${orders.length} 条订单数据`);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('导出订单失败:', error);
|
|
|
|
|
+ toast.error('导出订单数据失败,请稍后重试');
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ setExporting(false);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
// 获取订单状态对应的颜色
|
|
// 获取订单状态对应的颜色
|
|
|
const getStatusColor = (status: OrderStatus) => {
|
|
const getStatusColor = (status: OrderStatus) => {
|
|
|
switch (status) {
|
|
switch (status) {
|
|
@@ -194,6 +295,14 @@ export const OrdersPage = () => {
|
|
|
<div className="space-y-4">
|
|
<div className="space-y-4">
|
|
|
<div className="flex justify-between items-center">
|
|
<div className="flex justify-between items-center">
|
|
|
<h1 className="text-2xl font-bold">订单管理</h1>
|
|
<h1 className="text-2xl font-bold">订单管理</h1>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ onClick={handleExportOrders}
|
|
|
|
|
+ disabled={exporting}
|
|
|
|
|
+ className="flex items-center gap-2"
|
|
|
|
|
+ >
|
|
|
|
|
+ <Download className="h-4 w-4" />
|
|
|
|
|
+ {exporting ? '导出中...' : '导出Excel'}
|
|
|
|
|
+ </Button>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
{/* 订单统计面板 */}
|
|
{/* 订单统计面板 */}
|